All changes made by Claude Code, sorted by timestamp (UTC).
simulate_stream.py — full rewrite of the streaming demo script.
Previously uploaded all 100 files in one shot, making it impossible to demonstrate that human review improves subsequent AI categorization.
New two-phase structure:
- Phase 1: uploads the first N files (default 75). Some land in the Review Queue due to low AI confidence.
- Review pause: script halts and prints step-by-step instructions. User opens the dashboard, assigns categories to queued events, and clicks "Route to Pipeline". When done, presses Enter in the terminal.
- Phase 2: uploads the remaining 25 files. After each upload, waits 4 seconds then fetches
GET /pipeline/jobs/{job_id}and prints the AI's category and confidence score directly in the terminal. Events from sources the human reviewed will show higher confidence and coherent category labels — the feedback loop improvement is visible without opening the dashboard.
New flags: --batch1 N (controls phase 1 size, default 75), --dry-run (lists files per phase without uploading). Removed --mode burst (burst mode is not meaningful in a two-phase demo).
app/pipeline/normalizer.py
Problem: The feedback loop stored human-corrected categories in DynamoDB and applied them as a post-processing override after the AI had already run. This meant the AI's root_cause and recommended_action were generated without any knowledge of the confirmed category — a thermal event corrected by a human would get the thermal label but still carry the AI's original "unknown/unclear" root cause analysis.
Fix — call_ai():
- Added
category_hint: str | None = Noneparameter. - When a non-null, non-"unknown" hint is provided, a
PRIOR HUMAN FEEDBACKsection is appended to the prompt before the JSON instruction. It tells the AI which category engineers previously confirmed for this machine and instructs it to generateroot_causeandrecommended_actionassuming that category, assigning high confidence unless the message clearly contradicts it.
Fix — normalize_log():
- Moved
lookup_rule(record)to run beforecall_ai()(was after). - Extracts
category_hintfrom the rule if one exists. - Passes
category_hintintocall_ai()so the AI has the human context before it reasons, not after.
Effect: After one human review approves a category for a source, all future events from that source cause the AI to generate coherent analysis aligned with the confirmed category, rather than guessing blind and being silently relabelled downstream.
Three independent issues caused TimescaleDB to error on every full reset:
-
Duplicate ENUM creation (primary crash — always reproducible)
init/timescale/01_schema.sqlandinit/timescale/02_seed_data.sqlboth define the same four PostgreSQL ENUM types (severity_level,urgency_priority,log_format_type,path_type).docker-entrypoint-initdb.druns all scripts on an empty data directory;down -vdeletes the volume, so every reset re-runs both scripts.01_schema.sqlcreates the types successfully, then02_seed_data.sqlfails with"type already exists"on all four. -
WAL dirty shutdown (intermittent — adds recovery time)
docker compose downuses a 10-second SIGKILL deadline. PostgreSQL may not flush WAL cleanly in 10s, causing"database system was not properly shut down"+ WAL recovery on the next start. Recovery takes 10–30s and can exceed the health-check window, making dependent services fail theirservice_healthycondition. -
Volume deletion race (rare) If a container still holds a file descriptor when
down -vruns, Docker silently skips the volume deletion. The stale volume causes init scripts not to re-run, leaving the DB in an unexpected partial state.
init/timescale/01_schema.sql
- Wrapped all four bare
CREATE TYPEstatements inDO $$ BEGIN ... EXCEPTION WHEN duplicate_object THEN NULL; END $$blocks. They are now no-ops if the type already exists.
init/timescale/02_seed_data.sql
- Same treatment for the four
CREATE TYPE public.*blocks (lines 39–93 of the originalpg_dump). TheALTER TYPE ... OWNER TOlines that follow are unchanged (idempotent).
reset.sh (new file)
- Safe reset script that: (1) stops all containers with
--timeout 30to give PostgreSQL a clean shutdown window, (2) explicitly removes thetimescale-datavolume by name after containers are down, (3) starts the stack, (4) polls until TimescaleDB reportshealthybefore returning. - Usage:
chmod +x reset.sh && ./reset.sh
frontend/src/api/pipeline.ts
- Added
severity?: stringto theReviewDecisioninterface so the reviewer's chosen severity is sent to the backend with every submit.
frontend/src/pages/Dashboard.tsx (ReviewCard)
- Replaced the read-only severity badge in the card header with an editable
<select>dropdown (CRITICAL,ERROR,WARNING,INFO). Initialised to the AI-assigned severity. - Added
severitystate toReviewCard, updated from the dropdown. - Shows a subtle
"↑ changed by reviewer"hint when the reviewer's choice differs from the AI's original value. isCriticalnow reflects the reviewer's chosen severity, not the AI's — so the CRITICAL confirmation strip also responds to the reviewer upgrading a non-critical event.- All three submission paths (Route to Pipeline, Yes Forward, Dismiss) now pass the
severitystate. - Updated
onDecisioncallback signature to includeseverity: string. - Updated
handleDecisioninReviewQueueOverlayto accept and forwardseveritytosubmitReview.
app/pipeline/routes.py
- Added
severity: Optional[str]toReviewRequestpydantic model. - Kafka routing now uses
review.severity(reviewer's choice) first, falling back to the stored AI severity. The reviewer's severity is applied to the forwarded event dict so downstream consumers see the correct value.
Root cause analysis:
update_feedback_rule(source, category, approved) in dynamo.py wrote DynamoDB items keyed by (vendorId=source, fieldName=category) but never wrote a "category" attribute on the item itself. lookup_rule() in normalizer.py read back item.get("category") which was always None, so the category override in combine_and_score never fired. Human-corrected categories were stored in the DynamoDB key but completely invisible to the normalizer — they were silently discarded on every subsequent pipeline run.
app/shared/dynamo.py
update_feedback_rule(): now explicitly writes"category": {"S": category}as an attribute on the DynamoDB item, solookup_rule()can read it back and use it as a category override for future events from the same source.
app/pipeline/normalizer.py
lookup_rule(): replaceditems[0](arbitrary first result) withmax(items, key=approval_count)— selects the item with the most human approvals, which is the most confidently validated category for this source.- Confidence boost now sums across all category items for the source (previously only the first item's boost was used, which could be the least-approved category).
Files created:
generate_simulation_data.py— Script that generates 100 realistic semiconductor fab log files insimulation_data/. Covers 4 formats (JSON, LOG, CSV, XML), 3 severity tiers (P0 CRITICAL, P1 ERROR, P2 INFO/WARNING), and 10+ equipment types (PECVD, ETCH, LITHO, CMP, IMPLANT, etc.). 18 files tagged_INCIDENT_for burst mode detection.simulate_stream.py— Streaming simulation script with three modes:--mode demo(0.5–3s delays),--mode realistic(5–120s),--mode burst(groups of 3–8 rapid uploads then a long pause). Uploads files fromsimulation_data/toPOST /logs/uploadwith colour-coded terminal output.simulation_data/— Folder containing the 100 generated log files.
Root cause: CRITICAL events were never reaching the dashboard counter or the hot-path Kafka topic due to three independent bugs.
app/pipeline/parser.py
SEVERITY_MAP: changed"CRITICAL": "error"→"CRITICAL": "critical". The old mapping silently downgraded every CRITICAL event to ERROR before routing, making P0 permanently unreachable.
app/pipeline/router.py
- Replaced the broken P0 check (
severity == "error" and category in {"fire","safety",...}) withif severity == "critical": return TOPIC_P0. The old check required the AI category to be in a set that the AI prompt never produced (fire,safety, etc.), so P0 was structurally unreachable.
app/pipeline/main.py (Step 4.5 — Kafka send)
- Changed
await kafka_client.send_event(priority, event, key=source)toevent_to_send = {**event, "job_id": job_id}then sendevent_to_send. Thejob_idgenerated at the top ofprocess_log_file()was never merged into the event dict before Kafka send, causingjob_id=Nonein all Kafka messages.
app/consumers/hot_path.py
- Added
from uuid import uuid4import. - Changed
job_id = event.get("job_id", "unknown")→job_id = event.get("job_id") or str(uuid4()). The fallback"unknown"is not a valid UUID, causing every hot-path DB insert to fail with a PostgreSQL UUID validation error.
frontend/src/api/pipeline.ts
- Added
event_type: stringandmessage: stringfields to theReviewIteminterface. The backend SQL already returned these columns; they just weren't typed or displayed.
frontend/src/pages/Dashboard.tsx (ReviewCard component)
- Added a prominent blue "Event" box at the top of the card body showing
item.message(the actual log text — the most useful field for a human reviewer). - Added a metadata row showing
event_type,timestamp, andsource. - Added a visual divider between the event info section and the AI analysis section.
- Relabelled the category field hint to
"(editable)". - Renamed "View File" button to "Raw Data".
frontend/src/pages/ReviewQueue.css
- Added
.review-event-message,.review-message,.review-meta-row,.review-meta-item,.review-meta-label,.review-meta-value,.review-divider,.review-label-hintstyles.
app/pipeline/normalizer.py (AI prompt)
- Rewrote the AI prompt with semiconductor-fab-specific context and expanded the allowed category list from 4 generic options to 10 domain-specific ones:
thermal,mechanical,electrical,gas_leak,contamination,process_drift,safety,software,maintenance,unknown. - Added a category guide in the prompt explaining when to use each category to reduce "unknown" defaults.
- Made root cause and recommended action instructions more specific to fab operations.
app/pipeline/ingest.py
- Added optional
job_idparameter toingest_log(). Previously the function generated its own UUID internally, which diverged from thejob_idused inmain.py. Now both use the same UUID, so the MinIO keyraw_logs/{job_id}/{file_name}can be resolved from the DB.
app/pipeline/main.py
- Passes
job_id=job_idtoingest_log()so the MinIO key and all DB records share the same UUID. - Added a call to
insert_raw_log()after ingest (was imported but never called), recordingjob_idandfile_namein theraw_logstable so the raw-file endpoint can look up the filename.
app/shared/db.py
- Added
CREATE TABLE IF NOT EXISTS categoriestoinit_schema(), seeded with 10 built-in categories on startup usingON CONFLICT DO NOTHING. - Added
async def get_all_categories() -> List[str]—SELECT name FROM categories ORDER BY name. - Added
async def insert_category(name: str) -> bool— inserts a new category; returnsTrueif created,Falseif duplicate.
app/pipeline/routes.py
- Added
import asyncio,import re,from fastapi.responses import Response,from app.shared.minio_client import minio_clientimports. - Added
CategoryRequest(BaseModel)withname: str. - Added
GET /logs/{job_id}/raw— fetches the original file from MinIO (raw_logs/{job_id}/{file_name}in bucketraw-logs), returns astext/plain. Caps at 500 KB withX-Truncated: trueheader. Wraps the synchronous MinIO call inrun_in_executorto avoid blocking the async event loop. - Added
GET /categories— returns{"categories": [...]}from TimescaleDB. - Added
POST /categories— validates name format (lowercase, alphanumeric + underscores, 2–32 chars), inserts into DB, returns{"name": ..., "created": bool}. - Imported
get_all_categoriesandinsert_categoryfromapp.shared.db.
frontend/src/api/pipeline.ts
- Added
fetchRawLog(jobId)—GET /pipeline/logs/{jobId}/raw, returns{ content: string, truncated: boolean }. - Added
fetchCategories()—GET /pipeline/categories, returnsstring[]. - Added
addCategory(name)—POST /pipeline/categories, returns{ name, created }.
frontend/src/pages/Dashboard.tsx (ReviewCard)
- Renamed "✓ Approve" → "↑ Route to Pipeline" and "✗ Reject" → "— Dismiss" to make the downstream effect explicit.
- "Route to Pipeline" is disabled when
category === "unknown"or empty — forces the reviewer to assign a category before forwarding (the purpose of human review). - For CRITICAL severity events: first click on "Route to Pipeline" shows an inline yellow confirmation strip ("Forward this CRITICAL event to the live pipeline?") instead of submitting immediately. Second click confirms.
- Rewrote
handleViewFile→handleToggleRawto callfetchRawLog()instead offetchReviewQueueItem(), showing the actual original file content rather than a normalized DB JSON dump.
frontend/src/pages/Dashboard.tsx (ReviewCard + ReviewQueueOverlay)
- Replaced the free-text category
<input>with a<select>dropdown listing all known categories fetched from the backend. - Last
<option>is"+ Add new category…"— selecting it reveals an inline text input + Save/Cancel buttons. On save, callsaddCategory(), updates the shared category list, and sets the selected category. ReviewQueueOverlaynow fetches categories on mount (alongside the queue items) and passes them as acategoriesprop to eachReviewCard, plus anonCategoryAddedcallback so new categories appear immediately in all open cards.
frontend/src/pages/ReviewQueue.css
- Changed
.btn-rejectbackground from red#ef4444→ neutral grey#6b7280(Dismiss is not a danger action). - Added
.review-select— styled dropdown matching existing input design. - Added
.review-category-add,.btn-category-save,.btn-category-cancel,.review-add-error— inline add-new category row styles. - Added
.review-confirm-strip— yellow background confirmation strip for CRITICAL forward action. - Added
.btn-confirm-forward,.btn-confirm-cancel— buttons inside the confirmation strip. - Added
.review-file-loading,.review-file-error,.review-file-truncated— helpers for the raw file overlay.