- {meta.label}
+const StatCell = ({ label, value, tone }: { label: string; value: ReactNode; tone?: SeverityTone }) => (
+
+ {label}
+
+ {value}
- )
-}
+
+)
const StacktraceView = ({ stacktrace }: { stacktrace: string }) => (
-
+
{stacktrace}
)
@@ -133,20 +203,20 @@ const BreadcrumbsView = ({ issue }: { issue: Issue }) => {
if (!crumbs.length) return null
return (
-
-
+
+
Breadcrumbs before error
-
+
{crumbs.map((crumb, idx) => (
-
-
+
+
{new Date(crumb.t).toLocaleTimeString([], { hour12: false })}
-
+
{crumb.category}
- {crumb.message.slice(0, 200)}
+ {crumb.message.slice(0, 200)}
))}
@@ -158,7 +228,7 @@ const SampleContextChips = ({ sample }: { sample: IssueSample }) => {
const ctx = sampleContext(sample)
if (!ctx.length) return null
return (
-
+
{ctx.join(" · ")}
)
@@ -187,14 +257,15 @@ const StatusActions = ({
{actions.map(({ to, label, Icon }) => (
{
e.stopPropagation()
onStatusChange(issue, to)
}}
- className="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 transition-all hover:border-white/20 hover:text-white disabled:opacity-50"
+ className="inline-flex items-center gap-1.5 rounded-[9px] border border-ds-line-strong px-2.5 py-1.5 text-[12.5px] font-medium text-ds-text-2 transition-colors hover:bg-ds-surface-2 hover:text-ds-text disabled:opacity-50"
>
-
+
{label}
))}
@@ -213,31 +284,10 @@ const IssueCard = ({
}) => {
const { setActiveTraceId, setActiveSpanId, setActiveTab } = useObservabilityData()
const [expanded, setExpanded] = useState(false)
-
- const meta = kindMeta[issue.kind] ?? { label: issue.kind, Icon: Bug }
- const isFrontend = issue.kind === "frontend_error"
- const badgeGradient = isFrontend
- ? "from-violet-500 via-sky-500 to-cyan-400"
- : "from-rose-500 via-red-500 to-orange-400"
- const palette = getServicePalette(issue.service)
- const latestSample = issue.samples[issue.samples.length - 1]
- const stacktrace = latestStacktrace(issue)
- const hasMappedStacktrace = issue.samples.some((sample) => Boolean(sample.resolvedStacktrace))
- const contextSample = issue.samples.find((s) => s.httpMethod || s.httpRoute || s.httpStatus)
-
- const [needleResult, setNeedleResult] = useState<{
- action?: string
- tool?: string
- confidence?: number
- reasoning?: string
- latency_ms?: number
- arguments?: Record
- error?: string
- } | null>(null)
+ const [needleResult, setNeedleResult] = useState(null)
const [needleLoading, setNeedleLoading] = useState(false)
- const handleNeedleTriage = async (e: React.MouseEvent) => {
- e.stopPropagation()
+ const handleNeedleTriage = async () => {
setNeedleLoading(true)
try {
const response = await authFetch("/api/needle/classify", {
@@ -245,8 +295,7 @@ const IssueCard = ({
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ issue_id: issue.id }),
})
- const data = await response.json()
- setNeedleResult(data)
+ setNeedleResult(await response.json())
} catch (err) {
setNeedleResult({ error: err instanceof Error ? err.message : String(err) })
} finally {
@@ -254,66 +303,125 @@ const IssueCard = ({
}
}
+ const meta = kindMeta[issue.kind] ?? { label: issue.kind, Icon: Bug }
+ const tone = issueSeverityTone(issue)
+ const latestSample = issue.samples[issue.samples.length - 1]
+ const stacktrace = latestStacktrace(issue)
+ const hasMappedStacktrace = issue.samples.some((sample) => Boolean(sample.resolvedStacktrace))
+ const contextSample = issue.samples.find((s) => s.httpMethod || s.httpRoute || s.httpStatus)
+ const correlate = strongestCorrelate(issue)
+
const viewTrace = (traceId: string, spanId?: string) => {
setActiveTraceId(traceId)
if (spanId) setActiveSpanId(spanId)
setActiveTab("tab1")
}
+ const metaParts = [`service=${issue.service}`]
+ if (contextSample?.httpMethod && contextSample?.httpRoute) {
+ metaParts.push(`route=${contextSample.httpMethod} ${contextSample.httpRoute}`)
+ } else if (contextSample?.httpRoute) {
+ metaParts.push(`route=${contextSample.httpRoute}`)
+ }
+ if (issue.affectedServices.length > 1) {
+ metaParts.push(`affects=${issue.affectedServices.length} services`)
+ }
+
return (
-
-
+
+
+
setExpanded((v) => !v)}
+ aria-expanded={expanded}
+ aria-label={`${expanded ? "Collapse" : "Expand"} details for ${issue.title}`}
+ className="flex min-w-0 flex-1 flex-col gap-1.5 text-left"
+ >
+
+
+ {toneLabel[tone]}
+
+
+
+ {meta.label}
+
+
+ Detected {formatRelativeTime(issue.firstSeen / 1e6)} · last seen{" "}
+ {formatRelativeTime(issue.lastSeen / 1e6)}
+
+
+
+ {issue.title}
+
+ {metaParts.join(" · ")}
+
-
setExpanded((prev) => !prev)}
- className="flex cursor-pointer flex-wrap items-center gap-x-4 gap-y-2 p-3 pl-4"
- >
-
-
- {meta.label}
-
+
+ {latestSample?.traceId && (
+ viewTrace(latestSample.traceId!, latestSample.spanId)}
+ className="inline-flex h-9 items-center rounded-[9px] bg-ds-accent px-3.5 text-[13px] font-semibold text-ds-on-accent"
+ >
+ View trace
+
+ )}
+
+ setExpanded((v) => !v)}
+ aria-expanded={expanded}
+ aria-label={`${expanded ? "Collapse" : "Expand"} details for ${issue.title}`}
+ className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[9px] text-ds-text-3 hover:bg-ds-surface-2 hover:text-ds-text-2"
+ >
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+
+
-
- {issue.service}
-
+
+
+
-
- {issue.title}
-
-
-
-
-
- ×{issue.count}
-
-
-
-
-
- last seen {formatRelativeTime(issue.lastSeen / 1e6)}
-
-
- {expanded ?
:
}
+
+
+
+
+
+ Strongest correlate
+ {correlate ? (
+
+ {correlate.key}={correlate.value}{" "}
+
+ {Math.round(correlate.share * 100)}% of {correlate.total} occurrences
+
+
+ ) : (
+ NO SIGNAL
+ )}
+
{expanded && (
-
+
-
-
First seen {formatRelativeTime(issue.firstSeen / 1e6)}
- {issue.affectedServices.length > 1 && (
-
Affects: {issue.affectedServices.join(", ")}
- )}
+
{issue.status === "resolved" && issue.resolvedAt ? (
Resolved {formatRelativeTime(issue.resolvedAt / 1e6)}
) : null}
+ {issue.affectedServices.length > 1 && (
+ Affects: {issue.affectedServices.join(", ")}
+ )}
-
{stacktrace && (
-
+
{hasMappedStacktrace ? "Source-mapped stacktrace" : "Stacktrace"}
@@ -322,10 +430,10 @@ const IssueCard = ({
-
+
{(contextSample?.userAgent || latestSample?.userAgent) && (
-
-
+
+
{(contextSample?.userAgent || latestSample?.userAgent)?.slice(0, 80)}
)}
@@ -338,16 +446,67 @@ const IssueCard = ({
-
+ void handleNeedleTriage()}
+ disabled={needleLoading}
+ title="Classify this issue using local Needle 3 model"
+ className="inline-flex h-8 items-center gap-2 rounded-[9px] border border-ds-line-strong px-3 text-[12px] font-medium text-ds-text-2 hover:text-ds-text disabled:opacity-50"
+ >
+ {needleLoading ? (
+
+ ) : (
+
+ )}
+ {needleLoading ? "Needle 3 triaging…" : "Needle 3 triage"}
+
+ {needleResult && (
+
+
+
+ Needle 3 action
+
+ {needleResult.action || needleResult.tool || "Processed"}
+
+
+ {needleResult.confidence !== undefined && (
+
+ confidence{" "}
+ = 0.6 ? "text-ds-ok" : "text-ds-warn"}>
+ {(needleResult.confidence * 100).toFixed(0)}%
+
+ {needleResult.latency_ms ? ` (${needleResult.latency_ms}ms)` : ""}
+
+ )}
+
+ {needleResult.reasoning &&
“{needleResult.reasoning}”
}
+ {needleResult.arguments && Object.keys(needleResult.arguments).length > 0 && (
+
+ {Object.entries(needleResult.arguments).map(([k, v]) => (
+
+ {k}: {String(v)}
+
+ ))}
+
+ )}
+ {needleResult.error && (
+
{needleResult.error}
+ )}
+
+ )}
+
+
+
+
Recent occurrences ({issue.samples.length})
{[...issue.samples].reverse().map((sample, idx) => (
))}
-
-
-
- {needleLoading ? (
- <>
-
- Needle 3 Triaging...
- >
- ) : (
- <>
-
- Needle 3 Triage
- >
- )}
-
-
- {latestSample?.traceId && (
-
viewTrace(latestSample.traceId!, latestSample.spanId)}
- className="inline-flex items-center gap-2 rounded-xl bg-sky-500 px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-950 transition-all hover:bg-sky-400 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-sky-500/20"
- >
-
- View Latest Trace
-
- )}
-
-
- {needleResult && (
-
-
-
-
-
- Needle 3 Action:
-
-
- {needleResult.action || needleResult.tool || "Processed"}
-
-
- {needleResult.confidence !== undefined && (
-
- Confidence:{" "}
- = 0.6 ? "text-emerald-400" : "text-amber-400"}`}>
- {(needleResult.confidence * 100).toFixed(0)}%
-
- {needleResult.latency_ms && (
- ({needleResult.latency_ms}ms)
- )}
-
- )}
-
-
- {needleResult.reasoning && (
-
- “{needleResult.reasoning}”
-
- )}
-
- {needleResult.arguments && Object.keys(needleResult.arguments).length > 0 && (
-
-
- Extracted Arguments:
-
-
- {Object.entries(needleResult.arguments).map(([k, v]) => (
-
- {k}: {String(v)}
-
- ))}
-
-
- )}
-
- {needleResult.error && (
-
{needleResult.error}
- )}
-
- )}
)}
-
+
+ )
+}
+
+const ServiceDeviationPanel = () => {
+ const { uniqueServices, serviceHealth } = useObservabilityData()
+
+ const rows = useMemo(() => {
+ return uniqueServices
+ .map((name) => {
+ const health = serviceHealth[name]
+ const total = health?.totalSignals ?? 0
+ const errors = health?.errorCount ?? 0
+ return { name, total, errors, rate: total > 0 ? errors / total : 0 }
+ })
+ .sort((a, b) => b.errors - a.errors || b.rate - a.rate)
+ .slice(0, 8)
+ }, [uniqueServices, serviceHealth])
+
+ return (
+
+
+
Services by deviation
+
+ {uniqueServices.length} total
+
+ {rows.length === 0 ? (
+ Listening for service signals…
+ ) : (
+
+ {rows.map((row) => (
+
+
+ {row.name}
+
+
+
0 ? "bg-ds-critical" : "bg-ds-line-strong"}`}
+ style={{ width: `${Math.max(row.rate * 100, row.errors > 0 ? 6 : 0)}%` }}
+ />
+
+
0 ? "text-ds-critical" : "text-ds-text-3"
+ }`}
+ >
+ {row.errors > 0 ? `${(row.rate * 100).toFixed(1)}%` : "0%"}
+
+
+ ))}
+
+ )}
+
+ )
+}
+
+const IssueStatusPanel = ({ counts }: { counts: Record
}) => {
+ const total = counts.all || 1
+ const segments: { key: StatusFilter; tone: SeverityTone }[] = [
+ { key: "open", tone: "critical" },
+ { key: "resolved", tone: "ok" },
+ { key: "ignored", tone: "muted" },
+ ]
+ return (
+
+ Issue status
+
+ {segments.map((seg) => (
+
+ ))}
+
+
+ {segments.map((seg) => (
+
+
+ {seg.key}
+ {counts[seg.key]}
+
+ ))}
+
+
)
}
@@ -493,7 +653,8 @@ export const IssuesVisualizer = ({
onStatusChange,
statusBusy,
}: IssuesVisualizerProps) => {
- const { selectedServices, searchQuery } = useObservabilityData()
+ const { selectedServices, toggleService, resetFilters, searchQuery, setSearchQuery, uniqueServices } =
+ useObservabilityData()
const [statusFilter, setStatusFilter] = useState("all")
const [kindFilter, setKindFilter] = useState("all")
const [needleModalOpen, setNeedleModalOpen] = useState(false)
@@ -519,25 +680,10 @@ export const IssuesVisualizer = ({
}
}
- const runDetectorTest = async () => {
- setDetectorTestLoading(true)
- setDetectorTest(null)
- try {
- const response = await authFetch("/api/ml/test-spike", { method: "POST" })
- const data = (await response.json()) as DetectorTestResult
- if (!response.ok) throw new Error(data.error ?? `HTTP ${response.status}`)
- setDetectorTest(data)
- } catch (err) {
- setDetectorTest({ error: err instanceof Error ? err.message : String(err) })
- } finally {
- setDetectorTestLoading(false)
- }
- }
-
const spamIPSet = useMemo(() => new Set(spamIPs.map((entry) => entry.ip)), [spamIPs])
const statusCounts = useMemo(() => {
- const counts = { all: issues.length, open: 0, resolved: 0, ignored: 0 }
+ const counts: Record = { all: issues.length, open: 0, resolved: 0, ignored: 0 }
issues.forEach((issue) => {
counts[issueStatusOf(issue)] += 1
})
@@ -575,200 +721,313 @@ export const IssuesVisualizer = ({
})
}, [issues, selectedServices, searchQuery, statusFilter, kindFilter, excludeSpamIPs, spamIPSet])
+ const { primaryIssues, secondaryIssues } = useMemo(() => {
+ const sorted = [...filteredIssues].sort((a, b) => b.count - a.count)
+ if (statusFilter !== "all") return { primaryIssues: sorted, secondaryIssues: [] as Issue[] }
+ return {
+ primaryIssues: sorted.filter((i) => issueStatusOf(i) === "open"),
+ secondaryIssues: sorted.filter((i) => issueStatusOf(i) !== "open"),
+ }
+ }, [filteredIssues, statusFilter])
+
+ const [showSecondary, setShowSecondary] = useState(false)
+
+ const runDetectorTest = async () => {
+ setDetectorTestLoading(true)
+ setDetectorTest(null)
+ try {
+ const response = await authFetch("/api/ml/test-spike", { method: "POST" })
+ const data = (await response.json()) as DetectorTestResult
+ if (!response.ok) throw new Error(data.error ?? `HTTP ${response.status}`)
+ setDetectorTest(data)
+ } catch (err) {
+ setDetectorTest({ error: err instanceof Error ? err.message : String(err) })
+ } finally {
+ setDetectorTestLoading(false)
+ }
+ }
+
+ const activeFilterCount =
+ selectedServices.size + (statusFilter !== "all" ? 1 : 0) + (excludeSpamIPs ? 1 : 0) + (searchQuery ? 1 : 0)
+
+ const chips: FilterChip[] = [
+ ...Array.from(selectedServices).map((service) => ({
+ id: `service-${service}`,
+ filterKey: "service",
+ value: service,
+ tone: "neutral" as const,
+ onRemove: () => toggleService(service),
+ })),
+ ...(statusFilter !== "all"
+ ? [
+ {
+ id: "status",
+ filterKey: "status",
+ value: statusFilter,
+ tone: statusFilter === "resolved" ? ("ok" as const) : ("neutral" as const),
+ onRemove: () => setStatusFilter("all"),
+ },
+ ]
+ : []),
+ ...(excludeSpamIPs
+ ? [
+ {
+ id: "spam-ips",
+ filterKey: "spam-ips",
+ value: "hidden",
+ tone: "neutral" as const,
+ onRemove: () => setExcludeSpamIPs(false),
+ },
+ ]
+ : []),
+ ...(searchQuery
+ ? [
+ {
+ id: "query",
+ filterKey: "query",
+ value: searchQuery,
+ tone: "neutral" as const,
+ onRemove: () => setSearchQuery(""),
+ },
+ ]
+ : []),
+ ]
+
+ const addFilterOptions = uniqueServices
+ .filter((s) => !selectedServices.has(s))
+ .map((s) => ({ value: s, label: s }))
+
+ const clearAll = () => {
+ resetFilters()
+ setSearchQuery("")
+ setStatusFilter("all")
+ setExcludeSpamIPs(false)
+ }
+
if (loading && issues.length === 0) {
return (
-
-
- Loading issues...
+
)
}
return (
-
-
-
-
-
- Deduplicated Issues
-
+
+
setKindFilter(v as KindFilter),
+ }}
+ chips={chips}
+ addFilterOptions={addFilterOptions}
+ onAddFilter={(v) => toggleService(v)}
+ matchText={`Matching ${filteredIssues.length} of ${issues.length} issues · ${activeFilterCount} filter${activeFilterCount === 1 ? "" : "s"}`}
+ onClearAll={activeFilterCount > 0 ? clearAll : undefined}
+ />
- {/* Status filter chips */}
-
- {statusFilters.map(({ value, label }) => (
- setStatusFilter(value)}
- className={`rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider transition-all ${
- statusFilter === value
- ? "bg-sky-500/20 text-sky-300 ring-1 ring-sky-500/30"
- : "text-slate-500 hover:text-slate-300"
- }`}
- >
- {label}
- {statusCounts[value]}
-
- ))}
-
-
- setExcludeSpamIPs((value) => !value)}
- className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
- excludeSpamIPs
- ? "border-amber-400/40 bg-amber-400/15 text-amber-200"
- : "border-white/10 text-slate-500 hover:border-white/20 hover:text-slate-300"
- }`}
- title="Hide HTTP errors whose recorded source IP is on the honeypot list"
- >
-
- {excludeSpamIPs ? "Spam IPs hidden" : "Hide spam IPs"}
-
-
- {/* Kind filter chips */}
-
- {kindFilters.map(({ value, label, Icon }) => (
- setKindFilter(value)}
- className={`inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider transition-all ${
- kindFilter === value
- ? "bg-violet-500/20 text-violet-300 ring-1 ring-violet-500/30"
- : "text-slate-500 hover:text-slate-300"
- }`}
- >
-
- {label}
-
- ))}
-
-
-
- {filteredIssues.length} shown
-
-
-
-
- {error &&
Failed to load: {error} }
-
{
- const next = !showSpamIPs
- setShowSpamIPs(next)
- if (next && spamIPs.length === 0) void loadSpamIPs()
- }}
- className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white hover:border-white/20 transition-all"
- >
-
- Spam IPs ({spamIPs.length})
-
-
setNeedleModalOpen(true)}
- className="flex items-center gap-1.5 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-emerald-300 hover:bg-emerald-500/20 hover:border-emerald-500/50 hover:text-emerald-200 transition-all shadow-sm shadow-emerald-500/10"
- title="Open interactive Needle 3 telemetry classifier modal"
- >
-
- Test Needle 3
-
-
void runDetectorTest()}
- disabled={detectorTestLoading}
- className="flex items-center gap-1.5 rounded-lg border border-sky-500/30 bg-sky-500/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-sky-300 hover:bg-sky-500/20 hover:border-sky-500/50 hover:text-sky-200 transition-all disabled:cursor-wait disabled:opacity-60"
- title="Run a non-persistent synthetic spike through the ML feature and label pipeline"
- >
- {detectorTestLoading ? : }
- Test spike detector
-
-
refresh()}
- className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white hover:border-white/20 transition-all"
- >
-
- Refresh
-
-
-
-
- {detectorTest && (
-
-
-
- {detectorTest.passed ? "Spike detector verified" : "Spike detector test failed"}
-
-
- {detectorTest.error ?? detectorTest.scenario}
-
-
- {detectorTest.row && (
-
- baseline {(detectorTest.row.baselineErrorRate * 100).toFixed(1)}%
- future errors {detectorTest.row.futureErrorCount}
- future {(detectorTest.row.futureErrorRate * 100).toFixed(1)}%
- {detectorTest.row.labelKnown && detectorTest.row.label ? "label: spike" : "label: not spike"}
-
- )}
-
- )}
-
- {showSpamIPs && (
-
-
-
-
Promoted honeypot IPs
-
Used by the ingress tarpit synchronizer.
+
+
+
+
+ {statusFilter === "all" ? "Needs attention" : `${statusTabs.find((s) => s.value === statusFilter)?.label} issues`}
+
+
+ {statusFilter === "all" ? `${primaryIssues.length} open` : filteredIssues.length}
+
+
+
+ {statusTabs.map((t) => (
+ setStatusFilter(t.value)}
+ aria-pressed={statusFilter === t.value}
+ className={`rounded-[6px] px-2.5 py-1 text-[11.5px] font-semibold transition-colors ${
+ statusFilter === t.value ? "bg-ds-fill text-ds-text" : "text-ds-text-3 hover:text-ds-text-2"
+ }`}
+ >
+ {t.label}
+ {statusCounts[t.value]}
+
+ ))}
setShowSpamIPs(false)}
- className="rounded p-1 text-slate-500 hover:text-white"
- aria-label="Close spam IP list"
+ type="button"
+ onClick={() => setExcludeSpamIPs((v) => !v)}
+ aria-pressed={excludeSpamIPs}
+ title="Hide HTTP errors whose recorded source IP is on the honeypot list"
+ className={`inline-flex h-9 items-center gap-1.5 rounded-[9px] border px-2.5 text-[12px] font-medium transition-colors ${
+ excludeSpamIPs
+ ? "border-ds-warn/40 bg-ds-warn-bg text-ds-warn"
+ : "border-ds-line-strong text-ds-text-3 hover:text-ds-text-2"
+ }`}
>
-
+
+ {excludeSpamIPs ? "Spam IPs hidden" : "Hide spam IPs"}
+
+
setNeedleModalOpen(true)}
+ title="Open interactive Needle 3 telemetry classifier"
+ className="inline-flex h-9 items-center gap-1.5 rounded-[9px] border border-ds-line-strong px-2.5 text-[12px] font-medium text-ds-text-2 hover:text-ds-text"
+ >
+
+ Test Needle 3
+
+
void runDetectorTest()}
+ disabled={detectorTestLoading}
+ title="Run a non-persistent synthetic spike through the ML feature and label pipeline"
+ className="inline-flex h-9 items-center gap-1.5 rounded-[9px] border border-ds-line-strong px-2.5 text-[12px] font-medium text-ds-text-2 hover:text-ds-text disabled:cursor-wait disabled:opacity-60"
+ >
+ {detectorTestLoading ? (
+
+ ) : (
+
+ )}
+ Test spike detector
+
+
refresh()}
+ className="inline-flex h-9 items-center gap-1.5 rounded-[9px] border border-ds-line-strong px-2.5 text-[12px] font-medium text-ds-text-2 hover:text-ds-text"
+ >
+
+ Refresh
- {spamIPsLoading &&
Loading…
}
- {spamIPsError &&
Failed to load: {spamIPsError}
}
- {!spamIPsLoading && !spamIPsError && spamIPs.length === 0 && (
-
No promoted IPs.
+
+ {error && (
+
Failed to load: {error}
)}
- {spamIPs.length > 0 && (
-
- {spamIPs.map((entry) => (
-
-
{entry.ip}
-
×{entry.count}
+
+ {detectorTest && (
+
+
+
+ {detectorTest.passed ? "Spike detector verified" : "Spike detector test failed"}
+
+
{detectorTest.error ?? detectorTest.scenario}
+
+ {detectorTest.row && (
+
+ baseline {(detectorTest.row.baselineErrorRate * 100).toFixed(1)}%
+ future errors {detectorTest.row.futureErrorCount}
+ future {(detectorTest.row.futureErrorRate * 100).toFixed(1)}%
+ {detectorTest.row.labelKnown && detectorTest.row.label ? "label: spike" : "label: not spike"}
+ )}
+
+ )}
+
+ {showSpamIPs && (
+
+
+
+
Promoted honeypot IPs
+
Used by the ingress tarpit synchronizer.
+
+
setShowSpamIPs(false)}
+ aria-label="Close spam IP list"
+ className="rounded p-1 text-ds-text-3 hover:text-ds-text"
+ >
+
+
+
+ {spamIPsLoading &&
Loading…
}
+ {spamIPsError &&
Failed to load: {spamIPsError}
}
+ {!spamIPsLoading && !spamIPsError && spamIPs.length === 0 && (
+
No promoted IPs.
+ )}
+ {spamIPs.length > 0 && (
+
+ {spamIPs.map((entry) => (
+
+ {entry.ip}
+ ×{entry.count}
+
+ ))}
+
+ )}
+
+ )}
+
+ {!showSpamIPs && (
+
{
+ setShowSpamIPs(true)
+ if (spamIPs.length === 0) void loadSpamIPs()
+ }}
+ className="self-start text-[11.5px] text-ds-text-3 hover:text-ds-text-2"
+ >
+ View promoted spam IPs ({spamIPs.length})
+
+ )}
+
+ {primaryIssues.length === 0 ? (
+
+
+
+ {issues.length === 0 ? "No issues detected" : "Nothing matches your filters"}
+
+
+ {issues.length === 0
+ ? "Error-status spans and error/fatal-severity logs will show up here as they arrive."
+ : "Try widening the scope or clearing a filter."}
+
+
+ ) : (
+
+ {primaryIssues.map((issue) => (
+
))}
)}
-
- )}
- {filteredIssues.length === 0 ? (
-
-
-
- {issues.length === 0 ? "No issues detected" : "No issues match your filters"}
-
-
- {issues.length === 0
- ? "Error-status spans and error/fatal-severity logs will show up here as they arrive."
- : "Try adjusting your filters or search query."}
-
+ {secondaryIssues.length > 0 && (
+ <>
+
setShowSecondary((v) => !v)}
+ className="flex items-center gap-2.5 rounded-[12px] border border-dashed border-ds-line px-4 py-3 text-left text-[13px] text-ds-text-3 hover:text-ds-text-2"
+ >
+ {showSecondary ? (
+
+ ) : (
+
+ )}
+ {secondaryIssues.length} resolved or ignored
+
+ {showSecondary && (
+
+ {secondaryIssues.map((issue) => (
+
+ ))}
+
+ )}
+ >
+ )}
- ) : (
-
- {filteredIssues.map((issue) => (
-
- ))}
-
- )}
+
+
+
setNeedleModalOpen(false)} />
diff --git a/otel-ui/src/components/MetricsVisualizer.tsx b/otel-ui/src/components/MetricsVisualizer.tsx
index c0a4a9d..5d91848 100644
--- a/otel-ui/src/components/MetricsVisualizer.tsx
+++ b/otel-ui/src/components/MetricsVisualizer.tsx
@@ -291,10 +291,10 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
}, [selectedPod, isMultiPod, podLatestMap, latestPoint, aggMode, activePods]);
const gradients = [
- { from: "#818CF8", to: "#38BDF8" },
- { from: "#34D399", to: "#38BDF8" },
- { from: "#F472B6", to: "#FB7185" },
- { from: "#FBBF24", to: "#F59E0B" },
+ { from: "var(--ds-accent)", to: "var(--ds-accent)" },
+ { from: "var(--ds-ok)", to: "var(--ds-accent)" },
+ { from: "var(--ds-accent)", to: "var(--ds-critical)" },
+ { from: "var(--ds-warn)", to: "var(--ds-warn)" },
];
const color = gradients[index % gradients.length];
const gradientId = `gradient-${index}-${name.replace(/[^a-zA-Z0-9]/g, "_")}`;
@@ -303,9 +303,9 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
-
-
-
+
+
+
{new Date(data.time).toLocaleTimeString([], {
hour12: false,
hour: "2-digit",
@@ -315,7 +315,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
})}
-
+
{selectedPod !== "all"
? "Solo Value"
: isMultiPod
@@ -328,7 +328,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
: "Value"
: "Value"}
-
+
{valueFormatter.format(data.value ?? data.avg ?? data.total ?? 0)} {unit || ""}
@@ -336,32 +336,32 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
{/* If fleet average, show min-to-max envelope in tooltip */}
{isMultiPod && selectedPod === "all" && data.min !== undefined && data.max !== undefined && (
-
+
Spread:
- min {valueFormatter.format(data.min)} • max{" "}
- {valueFormatter.format(data.max)}
+ min {valueFormatter.format(data.min)} • max{" "}
+ {valueFormatter.format(data.max)}
)}
{/* Pod breakdown in tooltip */}
{isMultiPod && selectedPod === "all" && (
-
-
+
+
Pod Breakdown ({activePods.length}):
- click pod to solo
+ click pod to solo
{activePods.map((pod) => {
const pal = getPodPalette(pod);
const val = data[pod] ?? 0;
return (
-
+
{pod}
-
+
{valueFormatter.format(val)} {unit || ""}
@@ -371,16 +371,16 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
)}
- Service:
+ Service:
{service}
{data.attributes && data.attributes.length > 0 && (
-
+
{data.attributes.map((attr: OtelAttribute) => (
- {attr.key}:
-
+ {attr.key}:
+
{formatAttributeValue(attr.value)}
@@ -394,14 +394,14 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
};
return (
-
-
+
+
{type === "gauge" ? (
@@ -426,19 +426,19 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
{selectedPod !== "all" ? (
setSelectedPod("all")}
- className="inline-flex items-center gap-1 rounded bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider text-amber-300 border border-amber-500/30 hover:bg-amber-500/25 transition-all"
+ className="inline-flex items-center gap-1 rounded bg-ds-warn-bg px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider text-ds-warn border border-ds-warn/30 hover:bg-ds-warn-bg transition-all"
title="Click to exit solo mode and show all pods"
>
Solo: {selectedPod}
-
+
) : isMultiPod ? (
-
+
{activePods.length} pods
) : null}
@@ -446,18 +446,18 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
{type}
-
+
{name}
-
+
{stats?.count} samples {unit ? `• ${unit}` : ""}
@@ -466,13 +466,13 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
{/* Multi-pod aggregation selector if in chart mode and not soloed */}
{viewMode === "chart" && isMultiPod && selectedPod === "all" && (
-
+
setAggMode("avg")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
aggMode === "avg"
- ? "bg-sky-500/20 text-sky-300 shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-accent-bg text-ds-accent shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Fleet Average (mean across pods with min-max envelope)"
>
@@ -482,8 +482,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
onClick={() => setAggMode("lines")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
aggMode === "lines"
- ? "bg-sky-500/20 text-sky-300 shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-accent-bg text-ds-accent shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Overlay Lines (individual per-pod comparison)"
>
@@ -493,8 +493,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
onClick={() => setAggMode("max")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
aggMode === "max"
- ? "bg-sky-500/20 text-sky-300 shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-accent-bg text-ds-accent shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Fleet Maximum (peak / outlier pod over time)"
>
@@ -504,8 +504,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
onClick={() => setAggMode("sum")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
aggMode === "sum"
- ? "bg-sky-500/20 text-sky-300 shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-accent-bg text-ds-accent shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Fleet Total (sum of all pods)"
>
@@ -515,8 +515,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
onClick={() => setAggMode("stacked")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
aggMode === "stacked"
- ? "bg-sky-500/20 text-sky-300 shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-accent-bg text-ds-accent shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Stacked Area (cumulative volume)"
>
@@ -526,13 +526,13 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
)}
{/* View switcher: Gauge vs Timeline Chart */}
-
+
setViewMode("chart")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
viewMode === "chart"
- ? "bg-white/10 text-white shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-fill text-ds-text shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Time series area chart"
>
@@ -542,8 +542,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
onClick={() => setViewMode("gauge")}
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
viewMode === "gauge"
- ? "bg-cyan-500/20 text-cyan-300 shadow"
- : "text-slate-500 hover:text-slate-300"
+ ? "bg-ds-accent-bg text-ds-accent shadow"
+ : "text-ds-text-3 hover:text-ds-text-2"
}`}
title="Gauge dial view"
>
@@ -552,15 +552,15 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
-
+
{currentDisplay.label}
-
+
{valueFormatter.format(currentDisplay.value)}
- {unit && {unit} }
+ {unit && {unit} }
{currentDisplay.subtext && (
-
+
{currentDisplay.subtext}
)}
@@ -598,8 +598,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
-
-
+
+
{activePods.map((pod) => {
const pal = getPodPalette(pod);
@@ -612,7 +612,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
);
})}
-
+
-
-
+
+
+
Pods ({activePods.length}):
{selectedPod !== "all" && (
setSelectedPod("all")}
- className="inline-flex items-center gap-1 rounded-lg px-2 py-0.5 text-[10px] font-mono font-bold bg-white/10 hover:bg-white/20 text-white transition-all border border-white/10"
+ className="inline-flex items-center gap-1 rounded-lg px-2 py-0.5 text-[10px] font-mono font-bold bg-ds-fill hover:bg-ds-fill text-ds-text transition-all border border-ds-line"
title="Show all pods"
>
All Pods
@@ -794,57 +794,57 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
onClick={() => setSelectedPod(isSelected ? "all" : ps.pod)}
className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-0.5 text-[10px] font-mono border transition-all ${
isSelected
- ? "bg-amber-500/20 text-amber-200 border-amber-400/50 ring-2 ring-amber-400/30 shadow-md font-bold"
- : `${pal.bg} ${pal.border} hover:border-white/20 hover:brightness-110`
+ ? "bg-ds-warn-bg text-ds-warn border-ds-warn/50 ring-2 ring-ds-warn/40 shadow-md font-bold"
+ : `${pal.bg} ${pal.border} hover:border-ds-line-strong hover:brightness-110`
}`}
title={`Click to ${isSelected ? "unsolo" : "solo"} ${ps.pod} (${valueFormatter.format(ps.value)} ${unit || ""})`}
>
{ps.pod}
-
+
{valueFormatter.format(ps.value)}
- {isSelected && SOLO }
+ {isSelected && SOLO }
);
})}
)}
-
+
-
+
Min
-
+
{stats ? compactFormatter.format(stats.min) : "—"}
-
+
Avg
-
+
{stats ? compactFormatter.format(stats.avg) : "—"}
-
+
Max
-
+
{stats ? compactFormatter.format(stats.max) : "—"}
setIsExpanded(!isExpanded)}
- className="flex flex-col items-center justify-center hover:text-white transition-colors"
+ className="flex flex-col items-center justify-center hover:text-ds-text transition-colors"
>
-
+
Attributes
-
+
{latestPoint?.attributes?.length ?? 0}
@@ -852,21 +852,21 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
{isExpanded && latestPoint && (
-
+
{latestPoint.attributes && latestPoint.attributes.length > 0 ? (
latestPoint.attributes.map((attr: OtelAttribute) => (
- {attr.key}
-
+ {attr.key}
+
{formatAttributeValue(attr.value)}
))
) : (
-
+
No attributes present
)}
@@ -917,12 +917,12 @@ export const MetricsVisualizer = () => {
if (metricList.length === 0) {
return (
-
-
-
+
+
+
-
No metric signals detected
+
No metric signals detected
The telemetry pipeline is established and waiting for metrics. Send gauge, counter, or histogram data to see interactive gauges and trend visualizations.
@@ -938,12 +938,12 @@ export const MetricsVisualizer = () => {
-
-
+
+
Live Gauge Dashboard
-
+
{topGauges.length} Active Gauges
@@ -994,21 +994,21 @@ export const MetricsVisualizer = () => {
)}
{/* Metric List Toolbar */}
-
+
-
+
setSearch(e.target.value)}
placeholder="Filter metrics by name..."
- className="h-9 w-52 sm:w-64 rounded-xl border border-white/10 bg-slate-900/50 pl-8 pr-8 text-xs text-white placeholder-slate-500 focus:border-sky-500/50 focus:outline-none"
+ className="h-9 w-52 sm:w-64 rounded-xl border border-ds-line bg-ds-surface-2 pl-8 pr-8 text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-accent/50 focus:outline-none"
/>
{search && (
setSearch("")}
- className="absolute right-2.5 text-slate-500 hover:text-white"
+ className="absolute right-2.5 text-ds-text-3 hover:text-ds-text"
>
@@ -1019,7 +1019,7 @@ export const MetricsVisualizer = () => {
setSelectedServiceFilter(e.target.value)}
- className="h-9 rounded-xl border border-white/10 bg-slate-900/50 px-2.5 text-[11px] font-bold text-slate-300 outline-none focus:border-sky-500/50"
+ className="h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-2.5 text-[11px] font-bold text-ds-text-2 outline-none focus:border-ds-accent/50"
aria-label="Filter metrics by owning service"
>
All Services ({availableServices.length})
@@ -1032,13 +1032,13 @@ export const MetricsVisualizer = () => {
)}
{/* Type filters */}
-
+
setFilterType("all")}
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
filterType === "all"
- ? "bg-sky-500/20 text-sky-400 shadow-sm"
- : "text-slate-400 hover:text-white"
+ ? "bg-ds-accent-bg text-ds-accent shadow-sm"
+ : "text-ds-text-2 hover:text-ds-text"
}`}
>
All ({metricList.length})
@@ -1047,8 +1047,8 @@ export const MetricsVisualizer = () => {
onClick={() => setFilterType("gauge")}
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
filterType === "gauge"
- ? "bg-cyan-500/20 text-cyan-300 shadow-sm"
- : "text-slate-400 hover:text-white"
+ ? "bg-ds-accent-bg text-ds-accent shadow-sm"
+ : "text-ds-text-2 hover:text-ds-text"
}`}
>
Gauges
@@ -1057,8 +1057,8 @@ export const MetricsVisualizer = () => {
onClick={() => setFilterType("sum")}
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
filterType === "sum"
- ? "bg-violet-500/20 text-violet-300 shadow-sm"
- : "text-slate-400 hover:text-white"
+ ? "bg-ds-accent-bg text-ds-accent shadow-sm"
+ : "text-ds-text-2 hover:text-ds-text"
}`}
>
Sums
@@ -1067,8 +1067,8 @@ export const MetricsVisualizer = () => {
onClick={() => setFilterType("histogram")}
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
filterType === "histogram"
- ? "bg-amber-500/20 text-amber-300 shadow-sm"
- : "text-slate-400 hover:text-white"
+ ? "bg-ds-warn-bg text-ds-warn shadow-sm"
+ : "text-ds-text-2 hover:text-ds-text"
}`}
>
Histograms
@@ -1076,14 +1076,14 @@ export const MetricsVisualizer = () => {
-
+
Showing {filteredMetrics.length} of {metricList.length} metrics
{/* Metrics Grid */}
{filteredMetrics.length === 0 ? (
-
+
No metrics match your search criteria.
) : (
diff --git a/otel-ui/src/components/NeedleTestModal.tsx b/otel-ui/src/components/NeedleTestModal.tsx
index 8fb00d6..bee473b 100644
--- a/otel-ui/src/components/NeedleTestModal.tsx
+++ b/otel-ui/src/components/NeedleTestModal.tsx
@@ -73,22 +73,22 @@ export const NeedleTestModal: React.FC
= ({ isOpen, onClos
}
return (
-
-
+
+
{/* Header */}
-
+
-
+
-
+
Needle 3 AI Telemetry Classifier
-
+
Cactus-Compute
-
+
On-device ~35MB foundation model executing real-time tool calling and extraction on local CPU.
@@ -96,7 +96,7 @@ export const NeedleTestModal: React.FC
= ({ isOpen, onClos
@@ -104,7 +104,7 @@ export const NeedleTestModal: React.FC = ({ isOpen, onClos
{/* Preset Selector */}
-
+
Quick Telemetry Presets (from ClickHouse)
@@ -118,8 +118,8 @@ export const NeedleTestModal: React.FC
= ({ isOpen, onClos
}}
className={`rounded-lg border px-2.5 py-1 text-[11px] font-medium transition-all ${
inputText === p.text
- ? "border-emerald-500 bg-emerald-500/20 text-emerald-200"
- : "border-white/10 bg-white/5 text-slate-300 hover:border-white/20 hover:bg-white/10"
+ ? "border-ds-ok bg-ds-ok/15 text-ds-ok"
+ : "border-ds-line bg-ds-surface-2 text-ds-text-2 hover:border-ds-line-strong hover:bg-ds-fill"
}`}
>
{p.label}
@@ -130,14 +130,14 @@ export const NeedleTestModal: React.FC = ({ isOpen, onClos
{/* Input Textarea */}
-
+
Telemetry Event / Log / Trace Summary
@@ -148,7 +148,7 @@ export const NeedleTestModal: React.FC = ({ isOpen, onClos
type="button"
onClick={handleClassify}
disabled={loading || !inputText.trim()}
- className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-teal-400 px-4 py-2 text-xs font-bold text-slate-950 transition-all hover:opacity-95 active:scale-95 disabled:opacity-50 shadow-lg shadow-emerald-500/20"
+ className="inline-flex items-center gap-2 rounded-xl bg-ds-surface-2 px-4 py-2 text-xs font-bold text-ds-on-accent transition-all hover:opacity-95 active:scale-95 disabled:opacity-50 shadow-lg "
>
{loading ? (
<>
@@ -166,57 +166,57 @@ export const NeedleTestModal: React.FC = ({ isOpen, onClos
{/* Results Area */}
{result && (
-
-
+
+
{result.error ? (
-
+
) : (
-
+
)}
-
+
{result.error ? "Error" : "Triage Result"}
{result.action && (
-
+
{result.action}
)}
{result.confidence !== undefined && (
-
+
Confidence:{" "}
-
+
{(result.confidence * 100).toFixed(0)}%
{result.latency_ms !== undefined && (
- ({result.latency_ms}ms)
+ ({result.latency_ms}ms)
)}
)}
{result.reasoning && (
-
+
“{result.reasoning}”
)}
{result.arguments && Object.keys(result.arguments).length > 0 && (
-
+
Extracted Typed Arguments:
{Object.entries(result.arguments).map(([key, value]) => (
- {key}:
- {String(value)}
+ {key}:
+ {String(value)}
))}
@@ -224,7 +224,7 @@ export const NeedleTestModal: React.FC = ({ isOpen, onClos
)}
{result.error && (
-
+
{result.error}
)}
diff --git a/otel-ui/src/components/PatternsVisualizer.tsx b/otel-ui/src/components/PatternsVisualizer.tsx
index eb0dd1f..598f873 100644
--- a/otel-ui/src/components/PatternsVisualizer.tsx
+++ b/otel-ui/src/components/PatternsVisualizer.tsx
@@ -34,19 +34,19 @@ const PatternIssues = ({ pattern, issues, similar, review }: {
}
return (
-
+
- {members.map((issue) => void toggle(issue)} className="rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-left text-[10px] text-slate-300 hover:border-sky-400/40">{issue.title} ×{issue.count} )}
+ {members.map((issue) => void toggle(issue)} className="rounded-lg border border-ds-line bg-ds-surface-2 px-2 py-1 text-left text-[10px] text-ds-text-2 hover:border-ds-accent/40">{issue.title} ×{issue.count} )}
- {expanded && members[0] &&
-
Deterministic suggestions for {members[0].title}
+ {expanded && members[0] &&
+
Deterministic suggestions for {members[0].title}
{(related[members[0].id] ?? []).map((item) => {
const key = `${members[0].id}:${item.issue.id}`
return
- {item.issue.title} {Math.round(item.score * 100)}% {item.reasons.join(" · ")}
- void submitReview(members[0], item, "same")} className="ml-auto inline-flex items-center gap-1 rounded border border-emerald-400/20 px-2 py-1 text-emerald-300 hover:bg-emerald-400/10"> same
- void submitReview(members[0], item, "different")} className="inline-flex items-center gap-1 rounded border border-rose-400/20 px-2 py-1 text-rose-300 hover:bg-rose-400/10"> separate
- {item.decision && reviewed: {item.decision} }
+ {item.issue.title} {Math.round(item.score * 100)}% {item.reasons.join(" · ")}
+ void submitReview(members[0], item, "same")} className="ml-auto inline-flex items-center gap-1 rounded border border-ds-ok/20 px-2 py-1 text-ds-ok hover:bg-ds-ok/15"> same
+ void submitReview(members[0], item, "different")} className="inline-flex items-center gap-1 rounded border border-ds-critical/20 px-2 py-1 text-ds-critical hover:bg-ds-critical-bg"> separate
+ {item.decision && reviewed: {item.decision} }
})}
}
@@ -59,17 +59,17 @@ export function PatternsVisualizer() {
const [query, setQuery] = useState("")
const visible = useMemo(() => patterns.filter((pattern) => `${pattern.service} ${pattern.kind} ${pattern.signature}`.toLowerCase().includes(query.toLowerCase())), [patterns, query])
- if (loading && patterns.length === 0) return
Loading deterministic patterns…
+ if (loading && patterns.length === 0) return
Loading deterministic patterns…
return
-
-
Similarity Patterns
Exact issues stay separate; broader deterministic patterns are reviewable.
-
setQuery(event.target.value)} placeholder="Filter patterns…" className="ml-auto rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs text-slate-200 outline-none placeholder:text-slate-600 focus:border-sky-400/40" />
-
void refresh()} className="inline-flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white"> Refresh
+
+
Similarity Patterns
Exact issues stay separate; broader deterministic patterns are reviewable.
+
setQuery(event.target.value)} placeholder="Filter patterns…" className="ml-auto rounded-lg border border-ds-line bg-ds-surface-2 px-3 py-2 text-xs text-ds-text outline-none placeholder:text-ds-text-3 focus:border-ds-accent/40" />
+
void refresh()} className="inline-flex items-center gap-2 rounded-lg border border-ds-line px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 hover:text-ds-text"> Refresh
- {error &&
Failed to load patterns: {String(error)}
}
- {visible.length === 0 ?
No deterministic patterns observed yet.
:
{visible.map((pattern) =>
-
{pattern.service} {pattern.kind} {pattern.issueCount} exact issues · {pattern.occurrenceCount} occurrences last seen {formatRelativeTime(pattern.lastSeen / 1e6)}
-
{pattern.signature.split("\u0000").join(" · ")}
+ {error &&
Failed to load patterns: {String(error)}
}
+ {visible.length === 0 ?
No deterministic patterns observed yet.
:
{visible.map((pattern) =>
+
{pattern.service} {pattern.kind} {pattern.issueCount} exact issues · {pattern.occurrenceCount} occurrences last seen {formatRelativeTime(pattern.lastSeen / 1e6)}
+
{pattern.signature.split("\u0000").join(" · ")}
)}
}
diff --git a/otel-ui/src/components/RolloutsVisualizer.tsx b/otel-ui/src/components/RolloutsVisualizer.tsx
index bda5344..885fe99 100644
--- a/otel-ui/src/components/RolloutsVisualizer.tsx
+++ b/otel-ui/src/components/RolloutsVisualizer.tsx
@@ -5,9 +5,9 @@ import { formatRelativeTime } from "../lib/timeUtils"
import type { RolloutImpact } from "../lib/types"
const phaseStyle: Record
= {
- completed: { className: "text-emerald-300 bg-emerald-400/10 border-emerald-400/20", Icon: CheckCircle2 },
- failed: { className: "text-rose-300 bg-rose-400/10 border-rose-400/20", Icon: XCircle },
- progressing: { className: "text-amber-300 bg-amber-400/10 border-amber-400/20", Icon: Activity },
+ completed: { className: "text-ds-ok bg-ds-ok/15 border-ds-ok/20", Icon: CheckCircle2 },
+ failed: { className: "text-ds-critical bg-ds-critical-bg border-ds-critical/20", Icon: XCircle },
+ progressing: { className: "text-ds-warn bg-ds-warn-bg border-ds-warn/20", Icon: Activity },
}
export function RolloutsVisualizer() {
@@ -27,21 +27,21 @@ export function RolloutsVisualizer() {
catch (cause) { setImpactError(cause instanceof Error ? cause.message : String(cause)) }
}
- if (loading && rollouts.length === 0) return Loading cluster rollouts…
+ if (loading && rollouts.length === 0) return Loading cluster rollouts…
return
-
-
Cluster Rollouts
Read-only Kubernetes deployment state with optional Prometheus impact data.
-
void refresh()} className="ml-auto inline-flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white"> Refresh
+
+
Cluster Rollouts
Read-only Kubernetes deployment state with optional Prometheus impact data.
+
void refresh()} className="ml-auto inline-flex items-center gap-2 rounded-lg border border-ds-line px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 hover:text-ds-text"> Refresh
- {error &&
Rollout watcher unavailable: {String(error)}
}
- {impactError &&
Impact query failed: {impactError}
}
- {rollouts.length === 0 ?
No rollout data. Enable the Kubernetes watcher and grant its read-only RBAC permissions.
:
{rollouts.map((rollout) => {
+ {error &&
Rollout watcher unavailable: {String(error)}
}
+ {impactError &&
Impact query failed: {impactError}
}
+ {rollouts.length === 0 ?
No rollout data. Enable the Kubernetes watcher and grant its read-only RBAC permissions.
:
{rollouts.map((rollout) => {
const status = phaseStyle[rollout.phase] ?? phaseStyle.progressing
const Icon = status.Icon
const data = impactData[rollout.id]
- return
-
void select(rollout.id)} className="w-full text-left"> {rollout.phase}{rollout.namespace}/{rollout.workload} revision {rollout.revision || "unknown"} {formatRelativeTime(rollout.lastSeen / 1e6)}
release: {rollout.release || "unlabeled"} image: {rollout.image || "unknown"} replicas: {rollout.availableReplicas}/{rollout.desiredReplicas} ready reason: {rollout.reason || "unknown"}
- {selected === rollout.id &&
Started {formatRelativeTime(rollout.startedAt / 1e6)}{rollout.completedAt ? ` · completed ${formatRelativeTime(rollout.completedAt / 1e6)}` : ""}
{rollout.message || "No rollout message reported."}
{data &&
Prometheus error-rate points
{!data.configured ?
Prometheus is not configured.
: data.points.length === 0 ?
No matching Prometheus series.
:
{data.points.slice(-12).map((point) => {new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}: {point.value} )}
}
}
}
+ return
+
void select(rollout.id)} className="w-full text-left"> {rollout.phase}{rollout.namespace}/{rollout.workload} revision {rollout.revision || "unknown"} {formatRelativeTime(rollout.lastSeen / 1e6)}
release: {rollout.release || "unlabeled"} image: {rollout.image || "unknown"} replicas: {rollout.availableReplicas}/{rollout.desiredReplicas} ready reason: {rollout.reason || "unknown"}
+ {selected === rollout.id &&
Started {formatRelativeTime(rollout.startedAt / 1e6)}{rollout.completedAt ? ` · completed ${formatRelativeTime(rollout.completedAt / 1e6)}` : ""}
{rollout.message || "No rollout message reported."}
{data &&
Prometheus error-rate points
{!data.configured ?
Prometheus is not configured.
: data.points.length === 0 ?
No matching Prometheus series.
:
{data.points.slice(-12).map((point) => {new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}: {point.value} )}
}
}
}
})}
}
diff --git a/otel-ui/src/components/SearchAndWatcher.tsx b/otel-ui/src/components/SearchAndWatcher.tsx
index 5d90bac..5bcfeaa 100644
--- a/otel-ui/src/components/SearchAndWatcher.tsx
+++ b/otel-ui/src/components/SearchAndWatcher.tsx
@@ -62,10 +62,10 @@ export const SearchAndWatcher: React.FC = () => {
if (searchTimeRange !== "all") {
const msMap: Record
= {
- "1h": 60 * 60 * 1000,
- "6h": 6 * 60 * 60 * 1000,
- "24h": 24 * 60 * 60 * 1000,
- "7d": 7 * 24 * 60 * 60 * 1000,
+ "1h": 60 * 60 * 1000,
+ "6h": 6 * 60 * 60 * 1000,
+ "24h": 24 * 60 * 60 * 1000,
+ "7d": 7 * 24 * 60 * 60 * 1000,
};
const ms = msMap[searchTimeRange] || 24 * 60 * 60 * 1000;
const sinceNano = BigInt(Date.now() - ms) * 1_000_000n;
@@ -143,18 +143,18 @@ export const SearchAndWatcher: React.FC = () => {
return (
{/* Top Banner */}
-
+
-
+
-
+
Archive Search & Live Signal Trap
-
+
Expose deep telemetry data beyond the browser buffer. Query full ClickHouse & persistent storage logs/traces, or arm an OTLP signal trap to catch anomalies as they hit the server.
@@ -162,22 +162,22 @@ export const SearchAndWatcher: React.FC = () => {
{/* SECTION 1: Live Signal Trap */}
-
-
+
+
-
-
+
+
Live Signal Trap (/api/history/wait)
-
+
Long-poll subscriber for active debugging
-
+
Target Keyword / Pattern / Trace ID
@@ -187,20 +187,20 @@ export const SearchAndWatcher: React.FC = () => {
onChange={(e) => setWatchQuery(e.target.value)}
placeholder="e.g. 'error', '500', 'db_timeout', or service name..."
disabled={isWatching}
- className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white placeholder-slate-500 focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
+ className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-accent/50 focus:outline-none disabled:opacity-50 font-mono"
/>
-
+
Signal Type
setWatchType(e.target.value)}
disabled={isWatching}
- className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
+ className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none disabled:opacity-50 font-mono"
>
Any Telemetry
Traces Only
@@ -211,14 +211,14 @@ export const SearchAndWatcher: React.FC = () => {
-
+
Timeout
setWatchTimeout(Number(e.target.value))}
disabled={isWatching}
- className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
+ className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none disabled:opacity-50 font-mono"
>
15 seconds
30 seconds
@@ -230,7 +230,7 @@ export const SearchAndWatcher: React.FC = () => {
Cancel
@@ -239,7 +239,7 @@ export const SearchAndWatcher: React.FC = () => {
Arm Trap
@@ -253,16 +253,16 @@ export const SearchAndWatcher: React.FC = () => {
{watchStatus}
@@ -272,13 +272,13 @@ export const SearchAndWatcher: React.FC = () => {
{/* Captured Signal Preview */}
{caughtSignal && (
-
+
-
+
{caughtSignal.type}
-
+
Captured at {new Date(caughtSignal.timestamp / 1_000_000).toLocaleTimeString()}
@@ -292,7 +292,7 @@ export const SearchAndWatcher: React.FC = () => {
setActiveTab("tab1");
}
}}
- className="inline-flex items-center gap-1.5 rounded-lg bg-sky-500 px-3 py-1 text-[11px] font-bold text-slate-950 hover:bg-sky-400 transition-colors"
+ className="inline-flex items-center gap-1.5 rounded-lg bg-ds-accent px-3 py-1 text-[11px] font-bold text-ds-on-accent hover:bg-ds-accent transition-colors"
>
Jump to Captured Trace
@@ -300,7 +300,7 @@ export const SearchAndWatcher: React.FC = () => {
)}
-
+
{JSON.stringify(caughtSignal.data, null, 2)}
@@ -308,15 +308,15 @@ export const SearchAndWatcher: React.FC = () => {
{/* SECTION 2: Persistent Storage Search */}
-
-
+
+
-
-
+
+
Query Persistent Telemetry Store (/api/history/search)
-
+
Searches full database history beyond active memory
@@ -324,7 +324,7 @@ export const SearchAndWatcher: React.FC = () => {