From bdc2f8971293b781b51b9592fd3bbef1ab047fe1 Mon Sep 17 00:00:00 2001
From: spinsirr
Date: Sat, 27 Jun 2026 17:04:43 -0700
Subject: [PATCH] fix(calibration): replace dark server SVG charts with light
native visuals
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The bottom calibration charts were gbrain's dark-themed server-rendered SVGs
(#0a0a0f bg) embedded as
on Lore's light page — black boxes with dim,
hard-to-read text, and the per-domain/abandoned ones were empty anyway. Render
them natively from the profile JSON instead, on Lore's light theme:
- Brier vs baseline gauge (0–0.5 track, 0.25 baseline marker, green ≤0.25 else amber)
- Accuracy gauge (0–100%)
- Outcomes split bar (correct/incorrect/partial), reconstructed from the
scorecard aggregates via calibrationOutcomes() (pure, unit-tested)
Drops the CHARTS
fan-out (4 dark SVGs). typecheck + vitest (75) + next
build all green; routes stay dynamic.
Co-Authored-By: Zypher Agent
---
src/app/globals.css | 77 +++++++++++++++++++-------
src/components/admin/panels.tsx | 98 ++++++++++++++++++++++++++++-----
src/lib/admin-format.ts | 26 +++++++++
tests/admin-format.test.ts | 17 ++++++
4 files changed, 185 insertions(+), 33 deletions(-)
diff --git a/src/app/globals.css b/src/app/globals.css
index 7591f6c..c9ac06e 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -2447,30 +2447,69 @@ a.ext {
font-size: 14px;
line-height: 1.5;
}
-.admin-charts {
+/* Light-themed calibration visuals (replaced the dark server-rendered SVGs). */
+.calibration-grid-charts {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+.calibration-gauge {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 16px;
- margin-top: 16px;
+ gap: 8px;
}
-.admin-chart {
- margin: 0;
- border: 1px solid var(--hairline);
- border-radius: 12px;
- padding: 12px;
+.calibration-gauge-track {
+ position: relative;
+ height: 10px;
+ border-radius: 999px;
+ background: var(--hairline);
+ /* visible so the baseline marker can extend a touch above/below the track */
+ overflow: visible;
}
-.admin-chart img {
- width: 100%;
- height: auto;
- display: block;
+.calibration-gauge-fill {
+ position: absolute;
+ inset: 0 auto 0 0;
+ height: 100%;
+ border-radius: 999px;
+ min-width: 2px;
}
-.admin-chart figcaption {
- font-family: var(--font-mono);
- font-size: 11px;
- text-transform: uppercase;
- letter-spacing: 0.02em;
+.calibration-gauge-ok {
+ background: #1a7f37;
+}
+.calibration-gauge-warn {
+ background: #d29922;
+}
+.calibration-gauge-neutral {
+ background: var(--link);
+}
+.calibration-gauge-marker {
+ position: absolute;
+ top: -2px;
+ bottom: -2px;
+ width: 2px;
+ background: var(--ink);
+ z-index: 1;
+}
+.calibration-gauge small {
color: var(--muted);
- margin-top: 8px;
+ font-size: 12px;
+}
+.calibration-split {
+ display: flex;
+ height: 12px;
+ border-radius: 999px;
+ overflow: hidden;
+ background: var(--hairline);
+}
+.calibration-split .seg {
+ display: block;
+ height: 100%;
+}
+.calibration-split .seg-ok {
+ background: #1a7f37;
+}
+.calibration-split .seg-partial {
+ background: #d29922;
+}
+.calibration-split .seg-bad {
+ background: #c0392b;
}
.admin-toggle {
display: flex;
diff --git a/src/components/admin/panels.tsx b/src/components/admin/panels.tsx
index 6666e42..3e86570 100644
--- a/src/components/admin/panels.tsx
+++ b/src/components/admin/panels.tsx
@@ -10,6 +10,7 @@ import {
agentOptions,
calibrationGeneratedAt,
calibrationIssues,
+ calibrationOutcomes,
decimal,
dollars,
formatParams,
@@ -358,12 +359,40 @@ export function JobsPanel() {
);
}
-const CHARTS = [
- { type: "brier-trend", label: "Brier trend" },
- { type: "pattern-statements", label: "Pattern statements" },
- { type: "domain-bars", label: "Per-domain accuracy" },
- { type: "abandoned-threads", label: "Abandoned threads" },
-];
+// Light-themed horizontal gauge: a value on a 0..max track with an optional
+// baseline marker. Replaces the dark server-rendered SVG charts so the panel
+// stays on Lore's light theme (built from the profile JSON, no gbrain images).
+function CalibrationGauge({
+ value,
+ max,
+ marker,
+ tone,
+ caption,
+}: {
+ value: number | null | undefined;
+ max: number;
+ marker?: number;
+ tone: "ok" | "warn" | "neutral";
+ caption: string;
+}) {
+ const pct =
+ value == null || !Number.isFinite(value) ? 0 : Math.max(0, Math.min(1, value / max)) * 100;
+ const markerPct = marker != null ? Math.max(0, Math.min(1, marker / max)) * 100 : null;
+ return (
+
+
+
+ {markerPct != null && (
+
+ )}
+
+
{caption}
+
+ );
+}
function CalibrationMetric({
label,
@@ -410,6 +439,7 @@ export function CalibrationPanel() {
const issues = p ? calibrationIssues(p) : [];
const patterns = p?.pattern_statements ?? [];
const biasTags = p?.active_bias_tags ?? [];
+ const outcomes = p ? calibrationOutcomes(p) : null;
return (
No pattern statements returned.
)}
-
- {CHARTS.map((c) => (
-
- {/* server-proxied SVG, chart-type allowlisted */}
-
- {c.label}
-
- ))}
+
+
+
+
+
Outcomes
+ {outcomes ? (
+ <>
+
+ {outcomes.correct > 0 && (
+
+ )}
+ {outcomes.partial > 0 && (
+
+ )}
+ {outcomes.incorrect > 0 && (
+
+ )}
+
+
+ {outcomes.correct} correct · {outcomes.incorrect} wrong
+ {outcomes.partial > 0 ? ` · ${outcomes.partial} partial` : ""} · n=
+ {outcomes.total}
+
+ >
+ ) : (
+
No resolved outcomes yet.
+ )}
+
Raw profile
diff --git a/src/lib/admin-format.ts b/src/lib/admin-format.ts
index 5c2e0a5..02102df 100644
--- a/src/lib/admin-format.ts
+++ b/src/lib/admin-format.ts
@@ -67,6 +67,32 @@ export interface CalibrationIssue {
detail: string;
}
+export interface CalibrationOutcomes {
+ correct: number;
+ incorrect: number;
+ partial: number;
+ total: number;
+}
+
+// Reconstruct the correct/incorrect/partial counts from the scorecard's own
+// aggregates — the inverse of how gbrain computes them:
+// total = correct + incorrect + partial
+// partial_rate = partial / total
+// accuracy = correct / (correct + incorrect) (partial excluded)
+// Exact for clean samples (integer rounding for the rest). Returns null when
+// there's nothing to show or accuracy is unavailable.
+export function calibrationOutcomes(profile: CalibrationProfile): CalibrationOutcomes | null {
+ const total = profile.total_resolved ?? 0;
+ if (total <= 0) return null;
+ const acc = profile.accuracy;
+ if (acc == null || !Number.isFinite(acc)) return null;
+ const partial = Math.min(total, Math.max(0, Math.round((profile.partial_rate ?? 0) * total)));
+ const nonPartial = total - partial;
+ const correct = Math.min(nonPartial, Math.max(0, Math.round(acc * nonPartial)));
+ const incorrect = nonPartial - correct;
+ return { correct, incorrect, partial, total };
+}
+
// "just now" / "5m ago" / "3h ago" / "2d ago".
export function relativeTime(iso: string | null | undefined, now = Date.now()): string {
if (!iso) return "—";
diff --git a/tests/admin-format.test.ts b/tests/admin-format.test.ts
index 62f3ba3..116bba3 100644
--- a/tests/admin-format.test.ts
+++ b/tests/admin-format.test.ts
@@ -4,6 +4,7 @@ import {
agentOptions,
calibrationGeneratedAt,
calibrationIssues,
+ calibrationOutcomes,
decimal,
dollars,
formatParams,
@@ -14,6 +15,22 @@ import {
} from "../src/lib/admin-format.js";
import { agents, calibrationProfile, jobsSnapshot, requestsPage } from "./fixtures/admin.js";
+test("calibrationOutcomes reconstructs correct/incorrect/partial from scorecard aggregates", () => {
+ // 8 resolved, 50% accuracy, 0 partial → 4 correct / 4 wrong (exact)
+ expect(
+ calibrationOutcomes({ ...calibrationProfile, total_resolved: 8, accuracy: 0.5, partial_rate: 0 }),
+ ).toEqual({ correct: 4, incorrect: 4, partial: 0, total: 8 });
+ // with partials: 10 resolved, 20% partial (=2), accuracy 75% over the 8 non-partial → 6/2
+ expect(
+ calibrationOutcomes({ ...calibrationProfile, total_resolved: 10, accuracy: 0.75, partial_rate: 0.2 }),
+ ).toEqual({ correct: 6, incorrect: 2, partial: 2, total: 10 });
+ // guards: no data / no accuracy → null
+ expect(calibrationOutcomes({ ...calibrationProfile, total_resolved: 0 })).toBeNull();
+ expect(
+ calibrationOutcomes({ ...calibrationProfile, total_resolved: 8, accuracy: null }),
+ ).toBeNull();
+});
+
test("formatParams matches upstream summary (query / slug / ~partial / limit / +N)", () => {
expect(formatParams(null)).toBeNull();
expect(formatParams({})).toBeNull();