From c0b5b6dbeb48282ec55e87fa3126aab4f5e558d3 Mon Sep 17 00:00:00 2001
From: dirtydishes
Date: Sun, 17 May 2026 11:02:30 -0400
Subject: [PATCH 01/79] hydrate alert evidence from clickhouse
---
.beads/issues.jsonl | 1 +
apps/web/app/globals.css | 31 +++
apps/web/app/terminal.test.ts | 40 +++
apps/web/app/terminal.tsx | 229 +++++++++++++-----
...6-05-17-1101-clickhouse-alert-context.html | 194 +++++++++++++++
packages/storage/src/clickhouse.ts | 102 ++++++++
packages/storage/tests/alerts.test.ts | 106 ++++++++
services/api/src/alert-context.ts | 21 ++
services/api/src/index.ts | 21 ++
services/api/tests/alert-context.test.ts | 18 ++
10 files changed, 701 insertions(+), 62 deletions(-)
create mode 100644 docs/turns/2026-05-17-1101-clickhouse-alert-context.html
create mode 100644 services/api/src/alert-context.ts
create mode 100644 services/api/tests/alert-context.test.ts
diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
index 8bb2603..b2f3a4a 100644
--- a/.beads/issues.jsonl
+++ b/.beads/issues.jsonl
@@ -1,3 +1,4 @@
+{"_type":"issue","id":"islandflow-jbi","title":"Hydrate alert evidence details from ClickHouse","description":"Alert detail drawers need to fetch persisted alert context from ClickHouse by trace id, including linked flow packets, option prints, preserved execution context, and explicit missing refs for UI diagnostics.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:55:43Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:01:58Z","started_at":"2026-05-17T14:55:53Z","closed_at":"2026-05-17T15:01:58Z","close_reason":"Implemented ClickHouse-backed alert context hydration across storage, API, terminal drawer, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8kj","title":"Configure persistent beads Dolt remote on deltaisland server","description":"Install the beads and Dolt CLIs on the server, configure a persistent Dolt sync remote backed by the server-hosted Forgejo repository, verify refs/dolt/data publication, and document Nginx Proxy Manager / firewall considerations.","status":"closed","priority":1,"issue_type":"task","assignee":"delta","created_at":"2026-05-17T10:31:31Z","created_by":"delta","updated_at":"2026-05-17T10:37:47Z","started_at":"2026-05-17T10:32:16Z","closed_at":"2026-05-17T10:37:47Z","close_reason":"Installed bd and dolt on the server, configured the Forgejo-backed Dolt remote, published refs/dolt/data, and documented the setup.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-200","title":"Implement durable options tape history","description":"Implement the plan from docs/plans/2026-05-16-1711-durable-options-tape-history.html: durable ClickHouse-backed options history, signal/all prints view selection, preserved execution context, stale semantics limited to live health, reset runbook, tests, and turn documentation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-16T21:21:30Z","created_by":"dirtydishes","updated_at":"2026-05-16T21:26:51Z","started_at":"2026-05-16T21:21:33Z","closed_at":"2026-05-16T21:26:51Z","close_reason":"Implemented durable options tape history, signal/raw view selection, reset runbook, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-k4f","title":"Gate deploy script on docker workspace snapshot sync","description":"Prevent frozen-lockfile build failures during deploy by adding a local preflight in scripts/deploy.ts that runs bun run check:docker-workspace and aborts with a clear sync+commit remediation message when stale.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:01:44Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:04:11Z","started_at":"2026-05-15T23:01:48Z","closed_at":"2026-05-15T23:04:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 46f20bb..64b6f16 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -1818,6 +1818,28 @@ h3 {
gap: 10px;
}
+.drawer-context-loading {
+ padding: 12px 0 2px;
+}
+
+.drawer-skeleton {
+ width: 64%;
+ height: 12px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, var(--bg-soft), rgba(245, 166, 35, 0.14), var(--bg-soft));
+ background-size: 180% 100%;
+ animation: drawer-skeleton 1.2s ease-out infinite;
+}
+
+.drawer-skeleton-wide {
+ width: 100%;
+}
+
+.drawer-evidence-context {
+ margin-top: 8px;
+ color: var(--text-faint);
+}
+
.drawer-row {
padding: 12px 14px;
border-radius: 12px;
@@ -1825,6 +1847,15 @@ h3 {
background: var(--bg-soft);
}
+@keyframes drawer-skeleton {
+ 0% {
+ background-position: 100% 0;
+ }
+ 100% {
+ background-position: -100% 0;
+ }
+}
+
@keyframes pulse {
0% {
transform: scale(1);
diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts
index b6214eb..2be3da8 100644
--- a/apps/web/app/terminal.test.ts
+++ b/apps/web/app/terminal.test.ts
@@ -3,9 +3,11 @@ import { getSubscriptionKey as getLiveSubscriptionKey } from "@islandflow/types"
import {
NAV_ITEMS,
appendHistoryTail,
+ buildAlertContextPath,
buildDefaultFlowFilters,
buildOptionTapeQueryParams,
classifierToneForFamily,
+ collectAlertContextEvidence,
composeTapeItems,
deriveAlertDirection,
countActiveFlowFilterGroups,
@@ -95,6 +97,44 @@ describe("pinned evidence pruning", () => {
});
});
+describe("alert context hydration helpers", () => {
+ it("builds the persisted ClickHouse context endpoint path", () => {
+ expect(buildAlertContextPath("alert:large_call/one")).toBe(
+ "/flow/alerts/alert%3Alarge_call%2Fone/context"
+ );
+ });
+
+ it("merges hydrated packets and prints into pinned evidence maps", () => {
+ const packet = {
+ trace_id: "flowpacket:1",
+ id: "flowpacket:1",
+ members: ["print:1"],
+ source_ts: 1,
+ ingest_ts: 2,
+ seq: 1,
+ features: {},
+ join_quality: {}
+ } as any;
+ const print = makeOptionPrint({
+ trace_id: "print:1",
+ execution_nbbo_bid: 1.2,
+ execution_nbbo_ask: 1.3,
+ execution_underlying_spot: 450.05
+ });
+
+ const evidence = collectAlertContextEvidence({
+ alert: makeAlert({ evidence_refs: ["flowpacket:1", "print:1"] }),
+ flow_packets: [packet],
+ option_prints: [print],
+ missing_refs: []
+ });
+
+ expect(evidence.packets.get("flowpacket:1")).toBe(packet);
+ expect(evidence.prints.get("print:1")?.execution_nbbo_bid).toBe(1.2);
+ expect(evidence.prints.get("print:1")?.execution_underlying_spot).toBe(450.05);
+ });
+});
+
describe("live manifest", () => {
it("includes only tape channels on /tape", () => {
const filters = buildDefaultFlowFilters();
diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx
index 0dfc199..e1ee74c 100644
--- a/apps/web/app/terminal.tsx
+++ b/apps/web/app/terminal.tsx
@@ -4604,6 +4604,49 @@ type EvidenceItem =
| { kind: "print"; id: string; print: OptionPrint }
| { kind: "unknown"; id: string };
+type AlertContextBundle = {
+ alert: AlertEvent | null;
+ flow_packets: FlowPacket[];
+ option_prints: OptionPrint[];
+ missing_refs: string[];
+};
+
+type AlertContextStatus = {
+ traceId: string | null;
+ loading: boolean;
+ missingRefs: string[];
+ error: string | null;
+};
+
+export const buildAlertContextPath = (traceId: string): string =>
+ `/flow/alerts/${encodeURIComponent(traceId)}/context`;
+
+export const collectAlertContextEvidence = (
+ bundle: AlertContextBundle
+): {
+ packets: Map;
+ prints: Map;
+} => {
+ const packets = new Map();
+ const prints = new Map();
+
+ for (const packet of bundle.flow_packets) {
+ if (packet.id) {
+ packets.set(packet.id, packet);
+ }
+ if (packet.trace_id) {
+ packets.set(packet.trace_id, packet);
+ }
+ }
+ for (const print of bundle.option_prints) {
+ if (print.trace_id) {
+ prints.set(print.trace_id, print);
+ }
+ }
+
+ return { packets, prints };
+};
+
type DarkEvidenceItem =
| { kind: "join"; id: string; join: EquityPrintJoin }
| { kind: "unknown"; id: string };
@@ -4612,15 +4655,28 @@ type AlertDrawerProps = {
alert: AlertEvent;
flowPacket: FlowPacket | null;
evidence: EvidenceItem[];
+ contextStatus: AlertContextStatus;
onClose: () => void;
};
-const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps) => {
+const formatOptionalMoney = (value: unknown): string | null => {
+ const parsed = parseNumber(value, Number.NaN);
+ return Number.isFinite(parsed) ? `$${formatPrice(parsed)}` : null;
+};
+
+const formatOptionalMs = (value: unknown): string | null => {
+ const parsed = parseNumber(value, Number.NaN);
+ return Number.isFinite(parsed) ? `${Math.round(parsed)}ms` : null;
+};
+
+const AlertDrawer = ({ alert, flowPacket, evidence, contextStatus, onClose }: AlertDrawerProps) => {
const primary = alert.hits[0];
const direction = deriveAlertDirection(alert);
const severity = normalizeAlertSeverity(alert);
const evidencePrints = evidence.filter((item) => item.kind === "print");
const unknownCount = evidence.filter((item) => item.kind === "unknown").length;
+ const isContextLoading = contextStatus.traceId === alert.trace_id && contextStatus.loading;
+ const missingRefs = contextStatus.traceId === alert.trace_id ? contextStatus.missingRefs : [];
return (
) : (
- Flow packet not in the current live cache.
+ Persisted flow packet is not available for this alert.
)}
Evidence prints
{evidencePrints.length === 0 ? (
-
No evidence prints in the live cache yet.
+
Persisted evidence prints are not available for this alert.
) : (
{evidencePrints.slice(0, 6).map((item) => (
@@ -4709,6 +4775,36 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
${formatPrice(item.print.price)}
{formatSize(item.print.size)}x
{item.print.exchange}
+ {item.print.execution_nbbo_side ? Side {item.print.execution_nbbo_side} : null}
+ {formatOptionalMs(item.print.execution_nbbo_age_ms) ? (
+ Quote {formatOptionalMs(item.print.execution_nbbo_age_ms)}
+ ) : null}
+
+
+ {formatOptionalMoney(item.print.execution_nbbo_bid) ? (
+ Bid {formatOptionalMoney(item.print.execution_nbbo_bid)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_nbbo_ask) ? (
+ Ask {formatOptionalMoney(item.print.execution_nbbo_ask)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_nbbo_mid) ? (
+ Mid {formatOptionalMoney(item.print.execution_nbbo_mid)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_nbbo_spread) ? (
+ Spr {formatOptionalMoney(item.print.execution_nbbo_spread)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_underlying_spot) ? (
+ Spot {formatOptionalMoney(item.print.execution_underlying_spot)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_underlying_bid) ? (
+ U Bid {formatOptionalMoney(item.print.execution_underlying_bid)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_underlying_ask) ? (
+ U Ask {formatOptionalMoney(item.print.execution_underlying_ask)}
+ ) : null}
+ {formatOptionalMoney(item.print.execution_underlying_mid) ? (
+ U Mid {formatOptionalMoney(item.print.execution_underlying_mid)}
+ ) : null}
{formatTime(item.print.ts)}
@@ -4716,7 +4812,10 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
)}
{unknownCount > 0 ? (
- +{unknownCount} evidence prints not in cache.
+ +{unknownCount} evidence refs unresolved in persisted context.
+ ) : null}
+ {missingRefs.length > 0 ? (
+ Missing refs: {missingRefs.slice(0, 4).join(", ")}
) : null}
@@ -5548,6 +5647,12 @@ const useTerminalState = () => {
const [pinnedEquityJoinMap, setPinnedEquityJoinMap] = useState<
Map>
>(() => new Map());
+ const [selectedAlertContextStatus, setSelectedAlertContextStatus] = useState({
+ traceId: null,
+ loading: false,
+ missingRefs: [],
+ error: null
+ });
const [optionSupportSmartMoney, setOptionSupportSmartMoney] = useState([]);
const [optionSupportClassifierHits, setOptionSupportClassifierHits] = useState([]);
const [historicalNbboByTraceId, setHistoricalNbboByTraceId] = useState
) : (
- Flow packet not in the current live cache.
+ Flow packet not found in persisted alert context.
)}
Evidence prints
{evidencePrints.length === 0 ? (
-
No evidence prints in the live cache yet.
+
No persisted evidence prints available yet.
) : (
{evidencePrints.slice(0, 6).map((item) => (
@@ -4716,7 +4716,7 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
)}
{unknownCount > 0 ? (
-
+{unknownCount} evidence prints not in cache.
+
+{unknownCount} evidence prints unresolved from persisted context.
) : null}
@@ -4800,7 +4800,7 @@ const ClassifierHitDrawer = ({ hit, flowPacket, evidence, onClose }: ClassifierH
) : (
- Flow packet not in the current live cache.
+ Flow packet not found in persisted alert context.
)}
@@ -4824,7 +4824,7 @@ const ClassifierHitDrawer = ({ hit, flowPacket, evidence, onClose }: ClassifierH
)}
{unknownCount > 0 ? (
- +{unknownCount} evidence prints not in cache.
+ +{unknownCount} evidence prints unresolved from persisted context.
) : null}
@@ -4927,7 +4927,7 @@ const SmartMoneyDrawer = ({ event, flowPacket, evidence, onClose }: SmartMoneyDr
)}
{unknownCount > 0 ? (
- +{unknownCount} evidence prints not in cache.
+ +{unknownCount} evidence prints unresolved from persisted context.
) : null}
@@ -5039,7 +5039,7 @@ const DarkDrawer = ({ event, evidence, underlying, onClose }: DarkDrawerProps) =
)}
{unknownCount > 0 ? (
- +{unknownCount} evidence refs not in cache.
+ +{unknownCount} evidence refs unresolved from persisted context.
) : null}
@@ -5553,6 +5553,7 @@ const useTerminalState = () => {
const [historicalNbboByTraceId, setHistoricalNbboByTraceId] = useState