From 4dcc1a441a685629d9d90265456856123367c8a2 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 10 Jun 2026 16:02:24 -0700 Subject: [PATCH] feat(incidents): incident timeline visualizer + fix entry lookup using wrong DB path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds IncidentTimeline.vue — a pure SVG time-axis component rendered inside the incident detail drawer when entries are present: - Horizontal strip scaled to incident window (preserveAspectRatio=none) - Event ticks colored by severity, height proportional to severity level - 50-bin density shading shows burst periods as blue bands - Gap markers (dashed lines) for silence > 10% of window or > 60s - Hover tooltip showing nearest entry's severity, time, and truncated text - Click-to-scroll: clicking a tick highlights and scrolls to its entry in the list below - Legend showing only severity levels present in the incident Also fixes a pre-existing bug: get_incident_endpoint and both build_bundle callers were passing INCIDENTS_DB_PATH to get_incident_entries/build_bundle, causing all incident entry lookups to silently search the empty incidents DB instead of the main log DB. This made all incident detail views show "No log entries found". Closes: https://git.opensourcesolarpunk.com/Circuit-Forge/turnstone/issues/57 --- app/rest.py | 6 +- web/src/components/IncidentTimeline.vue | 290 ++++++++++++++++++++++++ web/src/views/IncidentsView.vue | 32 ++- 3 files changed, 321 insertions(+), 7 deletions(-) create mode 100644 web/src/components/IncidentTimeline.vue diff --git a/app/rest.py b/app/rest.py index a59ede9..4101253 100644 --- a/app/rest.py +++ b/app/rest.py @@ -1011,7 +1011,7 @@ def get_incident_endpoint(incident_id: str) -> dict: incident = get_incident(INCIDENTS_DB_PATH, incident_id) if not incident: raise HTTPException(status_code=404, detail="Incident not found") - entries = get_incident_entries(INCIDENTS_DB_PATH, incident) + entries = get_incident_entries(DB_PATH, incident) return { **dataclasses.asdict(incident), "entries": [dataclasses.asdict(e) for e in entries], @@ -1030,7 +1030,7 @@ def get_incident_bundle(incident_id: str, sanitize: bool = False) -> dict: incident = get_incident(INCIDENTS_DB_PATH, incident_id) if not incident: raise HTTPException(status_code=404, detail="Incident not found") - bundle = build_bundle(INCIDENTS_DB_PATH, incident, source_host=SOURCE_HOST, sanitize=sanitize) + bundle = build_bundle(DB_PATH, incident, source_host=SOURCE_HOST, sanitize=sanitize) record_sent_bundle(INCIDENTS_DB_PATH, incident_id, bundle, sanitized=sanitize) return bundle @@ -1048,7 +1048,7 @@ def send_incident_bundle(incident_id: str, sanitize: bool = False) -> dict: incident = get_incident(INCIDENTS_DB_PATH, incident_id) if not incident: raise HTTPException(status_code=404, detail="Incident not found") - bundle = build_bundle(INCIDENTS_DB_PATH, incident, source_host=SOURCE_HOST, sanitize=sanitize) + bundle = build_bundle(DB_PATH, incident, source_host=SOURCE_HOST, sanitize=sanitize) record_sent_bundle(INCIDENTS_DB_PATH, incident_id, bundle, sanitized=sanitize) payload = json.dumps(bundle).encode() req = urllib.request.Request( diff --git a/web/src/components/IncidentTimeline.vue b/web/src/components/IncidentTimeline.vue new file mode 100644 index 0000000..43ab564 --- /dev/null +++ b/web/src/components/IncidentTimeline.vue @@ -0,0 +1,290 @@ + + + diff --git a/web/src/views/IncidentsView.vue b/web/src/views/IncidentsView.vue index a8e021c..f1e8d6d 100644 --- a/web/src/views/IncidentsView.vue +++ b/web/src/views/IncidentsView.vue @@ -115,12 +115,25 @@
-

{{ selectedEntries.length }} entries in window

-
+ + + +
{{ shortTs(entry.timestamp_iso) }} {{ entry.severity || '?' }} @@ -138,6 +151,7 @@