hydrate alert evidence from clickhouse

This commit is contained in:
dirtydishes 2026-05-17 11:02:30 -04:00
parent cd0a1dd9e5
commit c0b5b6dbeb
10 changed files with 701 additions and 62 deletions

View file

@ -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);

View file

@ -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();

View file

@ -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<string, FlowPacket>;
prints: Map<string, OptionPrint>;
} => {
const packets = new Map<string, FlowPacket>();
const prints = new Map<string, OptionPrint>();
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 (
<aside className="drawer">
@ -4639,7 +4695,17 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
<span className={`pill severity-${severity}`}>{severity}</span>
<span className="drawer-chip">Score {Math.round(alert.score)}</span>
<span className={`pill direction-${direction}`}>{direction}</span>
{isContextLoading ? <span className="drawer-chip">Loading context</span> : null}
</div>
{isContextLoading ? (
<div className="drawer-section drawer-context-loading" aria-label="Loading persisted evidence">
<div className="drawer-skeleton drawer-skeleton-wide" />
<div className="drawer-skeleton" />
</div>
) : null}
{contextStatus.traceId === alert.trace_id && contextStatus.error ? (
<p className="drawer-empty">Persisted context could not be loaded: {contextStatus.error}</p>
) : null}
<div className="drawer-section">
<h4>Classifier hits</h4>
@ -4692,14 +4758,14 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
</p>
</div>
) : (
<p className="drawer-empty">Flow packet not in the current live cache.</p>
<p className="drawer-empty">Persisted flow packet is not available for this alert.</p>
)}
</div>
<div className="drawer-section">
<h4>Evidence prints</h4>
{evidencePrints.length === 0 ? (
<p className="drawer-empty">No evidence prints in the live cache yet.</p>
<p className="drawer-empty">Persisted evidence prints are not available for this alert.</p>
) : (
<div className="drawer-list">
{evidencePrints.slice(0, 6).map((item) => (
@ -4709,6 +4775,36 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
<span>${formatPrice(item.print.price)}</span>
<span>{formatSize(item.print.size)}x</span>
<span>{item.print.exchange}</span>
{item.print.execution_nbbo_side ? <span>Side {item.print.execution_nbbo_side}</span> : null}
{formatOptionalMs(item.print.execution_nbbo_age_ms) ? (
<span>Quote {formatOptionalMs(item.print.execution_nbbo_age_ms)}</span>
) : null}
</div>
<div className="drawer-row-meta drawer-evidence-context">
{formatOptionalMoney(item.print.execution_nbbo_bid) ? (
<span>Bid {formatOptionalMoney(item.print.execution_nbbo_bid)}</span>
) : null}
{formatOptionalMoney(item.print.execution_nbbo_ask) ? (
<span>Ask {formatOptionalMoney(item.print.execution_nbbo_ask)}</span>
) : null}
{formatOptionalMoney(item.print.execution_nbbo_mid) ? (
<span>Mid {formatOptionalMoney(item.print.execution_nbbo_mid)}</span>
) : null}
{formatOptionalMoney(item.print.execution_nbbo_spread) ? (
<span>Spr {formatOptionalMoney(item.print.execution_nbbo_spread)}</span>
) : null}
{formatOptionalMoney(item.print.execution_underlying_spot) ? (
<span>Spot {formatOptionalMoney(item.print.execution_underlying_spot)}</span>
) : null}
{formatOptionalMoney(item.print.execution_underlying_bid) ? (
<span>U Bid {formatOptionalMoney(item.print.execution_underlying_bid)}</span>
) : null}
{formatOptionalMoney(item.print.execution_underlying_ask) ? (
<span>U Ask {formatOptionalMoney(item.print.execution_underlying_ask)}</span>
) : null}
{formatOptionalMoney(item.print.execution_underlying_mid) ? (
<span>U Mid {formatOptionalMoney(item.print.execution_underlying_mid)}</span>
) : null}
</div>
<p className="drawer-note">{formatTime(item.print.ts)}</p>
</div>
@ -4716,7 +4812,10 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
</div>
)}
{unknownCount > 0 ? (
<p className="drawer-empty">+{unknownCount} evidence prints not in cache.</p>
<p className="drawer-empty">+{unknownCount} evidence refs unresolved in persisted context.</p>
) : null}
{missingRefs.length > 0 ? (
<p className="drawer-empty">Missing refs: {missingRefs.slice(0, 4).join(", ")}</p>
) : null}
</div>
</aside>
@ -5548,6 +5647,12 @@ const useTerminalState = () => {
const [pinnedEquityJoinMap, setPinnedEquityJoinMap] = useState<
Map<string, PinnedEntry<EquityPrintJoin>>
>(() => new Map());
const [selectedAlertContextStatus, setSelectedAlertContextStatus] = useState<AlertContextStatus>({
traceId: null,
loading: false,
missingRefs: [],
error: null
});
const [optionSupportSmartMoney, setOptionSupportSmartMoney] = useState<SmartMoneyEvent[]>([]);
const [optionSupportClassifierHits, setOptionSupportClassifierHits] = useState<ClassifierHitEvent[]>([]);
const [historicalNbboByTraceId, setHistoricalNbboByTraceId] = useState<Map<string, OptionNBBO | null>>(
@ -5593,69 +5698,67 @@ const useTerminalState = () => {
}, [pinnedOptionPrintMap.size, pinnedFlowPacketMap.size, pinnedEquityJoinMap.size]);
useEffect(() => {
if (!selectedAlert || mode !== "live") {
if (!selectedAlert) {
setSelectedAlertContextStatus({
traceId: null,
loading: false,
missingRefs: [],
error: null
});
return;
}
const packetId = selectedAlert.evidence_refs[0];
if (packetId && !resolvedFlowPacketMap.has(packetId)) {
incrementRetentionMetric("pinnedFetchMisses", 1);
void fetch(buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`))
.then(async (response) => {
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
return response.json();
})
.then((payload: { data?: FlowPacket | null }) => {
if (!payload.data) {
return;
}
const now = Date.now();
const next = new Map<string, FlowPacket>([[payload.data.id, payload.data]]);
setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, next, now));
})
.catch((error) => {
incrementRetentionMetric("pinnedFetchFailures", 1);
console.warn("Failed to fetch flow packet evidence", error);
});
}
const abort = new AbortController();
setSelectedAlertContextStatus({
traceId: selectedAlert.trace_id,
loading: true,
missingRefs: [],
error: null
});
incrementRetentionMetric("pinnedFetchMisses", selectedAlert.evidence_refs.length);
const missingPrintIds = selectedAlert.evidence_refs.filter(
(id) => !resolvedFlowPacketMap.has(id) && !resolvedOptionPrintMap.has(id)
);
if (missingPrintIds.length > 0) {
incrementRetentionMetric("pinnedFetchMisses", missingPrintIds.length);
const url = new URL(buildApiUrl("/option-prints/by-trace"));
for (const traceId of missingPrintIds) {
url.searchParams.append("trace_id", traceId);
}
void fetch(url.toString())
.then(async (response) => {
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
return response.json();
})
.then((payload: { data?: OptionPrint[] }) => {
const next = new Map<string, OptionPrint>();
for (const item of payload.data ?? []) {
if (!item || !item.trace_id) {
continue;
}
next.set(item.trace_id, item);
}
if (next.size > 0) {
const now = Date.now();
setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, now));
}
})
.catch((error) => {
incrementRetentionMetric("pinnedFetchFailures", 1);
console.warn("Failed to fetch option print evidence", error);
void fetch(buildApiUrl(buildAlertContextPath(selectedAlert.trace_id)), { signal: abort.signal })
.then(async (response) => {
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
return response.json();
})
.then((payload: AlertContextBundle) => {
if (abort.signal.aborted) {
return;
}
const { packets, prints } = collectAlertContextEvidence(payload);
const now = Date.now();
if (packets.size > 0) {
setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, packets, now));
}
if (prints.size > 0) {
setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, prints, now));
}
setSelectedAlertContextStatus({
traceId: selectedAlert.trace_id,
loading: false,
missingRefs: payload.missing_refs ?? [],
error: null
});
}
}, [selectedAlert, mode, resolvedFlowPacketMap, resolvedOptionPrintMap]);
})
.catch((error) => {
if (abort.signal.aborted) {
return;
}
incrementRetentionMetric("pinnedFetchFailures", 1);
console.warn("Failed to fetch persisted alert context", error);
setSelectedAlertContextStatus({
traceId: selectedAlert.trace_id,
loading: false,
missingRefs: [],
error: error instanceof Error ? error.message : String(error)
});
});
return () => abort.abort();
}, [selectedAlert]);
useEffect(() => {
if (!selectedDarkEvent || mode !== "live") {
@ -6802,6 +6905,7 @@ const useTerminalState = () => {
packetIdByOptionTraceId,
classifierDecorByOptionTraceId,
selectedEvidence,
selectedAlertContextStatus,
selectedFlowPacket,
selectedDarkEvidence,
selectedDarkUnderlying,
@ -8515,6 +8619,7 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
alert={state.selectedAlert}
flowPacket={state.selectedFlowPacket}
evidence={state.selectedEvidence}
contextStatus={state.selectedAlertContextStatus}
onClose={() => state.setSelectedAlert(null)}
/>
) : null}