Skip to content

Commit 44cda8a

Browse files
committed
fix(webapp): time the report sparkline's bars from the report, not the reader's clock
Bar timestamps came from Date.now() during render, so the same bar reported a different time on every re-render and a server pass disagreed with the client. They now come from the view model's generatedAt, which the schema already describes as the timestamp the renderer must not invent. Moves the arithmetic into report-spark.ts to keep it clock-free and testable, and drops the unreachable Math.max on the slice end while doing so.
1 parent a4edbcd commit 44cda8a

2 files changed

Lines changed: 49 additions & 34 deletions

File tree

apps/webapp/app/components/dashboard-agent/ReportView.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from "~/presenters/v3/reports/report-layout";
3131
import { type ReportMessages } from "~/presenters/v3/reports/report-messages";
3232
import { AgentBadge } from "./agent-badges";
33+
import { seriesEndMs as toSeriesEndMs } from "./report-spark";
3334
import {
3435
ReportBody,
3536
ReportCard,
@@ -214,7 +215,15 @@ const REFERENCE_URL_FALLBACK: Record<string, string> = {
214215

215216
// --- pieces -----------------------------------------------------------------
216217

217-
function MetricRow({ row, windowMinutes }: { row: LayoutMetricRow; windowMinutes: number }) {
218+
function MetricRow({
219+
row,
220+
windowMinutes,
221+
seriesEndMs,
222+
}: {
223+
row: LayoutMetricRow;
224+
windowMinutes: number;
225+
seriesEndMs: number | null;
226+
}) {
218227
// The hero row's annotation is spelled out rather than tucked in with the baseline.
219228
const annotation = row.note?.kind === "annotation" ? row.note.text : undefined;
220229

@@ -230,6 +239,7 @@ function MetricRow({ row, windowMinutes }: { row: LayoutMetricRow; windowMinutes
230239
series={row.series}
231240
windowMinutes={windowMinutes}
232241
anomalyMinutes={row.anomalyMinutes}
242+
seriesEndMs={seriesEndMs}
233243
formatPoint={(value) => fmtValue(value, row.unit)}
234244
/>
235245
);
@@ -242,15 +252,22 @@ function MetricRow({ row, windowMinutes }: { row: LayoutMetricRow; windowMinutes
242252
function FindingBody({
243253
finding,
244254
windowMinutes,
255+
seriesEndMs,
245256
}: {
246257
finding: LayoutFinding;
247258
windowMinutes: number;
259+
seriesEndMs: number | null;
248260
}) {
249261
return (
250262
<div className="space-y-2.5">
251263
<ReportMetricList>
252264
{finding.metrics.map((row) => (
253-
<MetricRow key={row.id} row={row} windowMinutes={windowMinutes} />
265+
<MetricRow
266+
key={row.id}
267+
row={row}
268+
windowMinutes={windowMinutes}
269+
seriesEndMs={seriesEndMs}
270+
/>
254271
))}
255272
</ReportMetricList>
256273

@@ -292,6 +309,7 @@ export function ReportView({
292309
}) {
293310
const layout = buildReportLayout(vm, messagesFor(vm.title));
294311
const severity = layout.headline.severity;
312+
const seriesEndMs = toSeriesEndMs(vm.generatedAt);
295313
const linkByKey = (key: string | undefined) =>
296314
key === undefined ? undefined : vm.links.find((link) => link.key === key)?.url;
297315

@@ -355,7 +373,11 @@ export function ReportView({
355373
{layout.trust ? <p className="text-sm text-warning">{layout.trust.note}</p> : null}
356374

357375
{layout.hero && layout.hero.expanded ? (
358-
<FindingBody finding={layout.hero} windowMinutes={vm.windowMinutes} />
376+
<FindingBody
377+
finding={layout.hero}
378+
windowMinutes={vm.windowMinutes}
379+
seriesEndMs={seriesEndMs}
380+
/>
359381
) : null}
360382

361383
{layout.findings.length > 0 || layout.statements.length > 0 ? (
@@ -371,7 +393,11 @@ export function ReportView({
371393
/>
372394
{finding.expanded ? (
373395
<div className="pl-[1.375rem]">
374-
<FindingBody finding={finding} windowMinutes={vm.windowMinutes} />
396+
<FindingBody
397+
finding={finding}
398+
windowMinutes={vm.windowMinutes}
399+
seriesEndMs={seriesEndMs}
400+
/>
375401
</div>
376402
) : null}
377403
</div>

apps/webapp/app/components/dashboard-agent/report-sparkline.tsx

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import TooltipPortal from "~/components/primitives/TooltipPortal";
3131
import { cn } from "~/utils/cn";
3232
import { AgentStatusIcon, type AgentTone } from "./agent-badges";
3333
import { AgentCard, AgentCardBody, AgentCardHeader } from "./agent-card";
34+
import { barTimesMs, condense, hotBarCount } from "./report-spark";
3435

3536
/** Both cards' severity type (`Severity` / `ReportSeverity`) resolves to this. */
3637
export type ReportSeverityKey = "ok" | "warn" | "crit";
@@ -461,20 +462,7 @@ const SPARK_WIDTH_CLASS = "w-[6.5rem]";
461462
/** The chart's own width; the trailing peak label uses the column's remainder. */
462463
const SPARK_WIDTH = 72;
463464

464-
/** How many bars a series is condensed to, so each bar stays hoverable. */
465-
const MAX_BARS = 18;
466-
467-
/** Average adjacent points down so each bar is wide enough to read and hover. */
468-
function condense(points: number[], maxBars: number): number[] {
469-
if (points.length <= maxBars) return points;
470-
const perBar = points.length / maxBars;
471-
return Array.from({ length: maxBars }, (_, i) => {
472-
const slice = points.slice(Math.floor(i * perBar), Math.max(Math.floor((i + 1) * perBar), 1));
473-
return slice.reduce((sum, v) => sum + v, 0) / Math.max(slice.length, 1);
474-
});
475-
}
476-
477-
type ReportSparkDatum = { count: number; date: Date; hot: boolean };
465+
type ReportSparkDatum = { count: number; date: Date | null; hot: boolean };
478466

479467
function ReportSparkTooltip({
480468
active,
@@ -486,9 +474,11 @@ function ReportSparkTooltip({
486474
return (
487475
<TooltipPortal active={active}>
488476
<div className="rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2">
489-
<Header3 className="border-b border-b-border-bright pb-2">
490-
{formatDateTime(entry.date, "UTC", [], false, true)}
491-
</Header3>
477+
{entry.date ? (
478+
<Header3 className="border-b border-b-border-bright pb-2">
479+
{formatDateTime(entry.date, "UTC", [], false, true)}
480+
</Header3>
481+
) : null}
492482
<div className="mt-2 text-xs tabular-nums text-text-bright">{formatPoint(entry.count)}</div>
493483
{entry.hot ? <div className="mt-1 text-xs text-warning">in the anomaly window</div> : null}
494484
</div>
@@ -511,6 +501,8 @@ export function ReportSparkline({
511501
* matching trailing bars paint at full strength.
512502
*/
513503
anomalyMinutes,
504+
/** When the series ends, from the report's `generatedAt`. Turns a bar into a time. */
505+
seriesEndMs,
514506
/** The metric's own formatter, used by the tooltip and the peak label. */
515507
formatPoint,
516508
label,
@@ -520,26 +512,20 @@ export function ReportSparkline({
520512
severity: ReportSeverityKey;
521513
windowMinutes: number;
522514
anomalyMinutes?: number;
515+
seriesEndMs: number | null;
523516
formatPoint: (value: number) => string;
524517
label: string;
525518
className?: string;
526519
}) {
527-
const bars = condense(points, MAX_BARS);
528-
const windowMs = windowMinutes * 60_000;
529-
// The view model carries buckets, not timestamps, so synthesise them from now
530-
// backwards.
531-
const barIntervalMs = bars.length > 0 ? windowMs / bars.length : windowMs;
532-
const startMs = Date.now() - windowMs;
533-
534-
const minutesPerBar = bars.length > 0 ? windowMinutes / bars.length : 0;
535-
const hotBars =
536-
anomalyMinutes && minutesPerBar > 0
537-
? Math.min(bars.length, Math.max(1, Math.round(anomalyMinutes / minutesPerBar)))
538-
: 0;
520+
const bars = condense(points);
521+
// The view model carries buckets, not timestamps, so spread them back from the
522+
// series' end. Never from the renderer's clock: see `report-spark.ts`.
523+
const times = barTimesMs(bars.length, windowMinutes, seriesEndMs);
524+
const hotBars = hotBarCount(bars.length, windowMinutes, anomalyMinutes);
539525

540526
const data: ReportSparkDatum[] = bars.map((count, i) => ({
541527
count,
542-
date: new Date(startMs + i * barIntervalMs),
528+
date: times[i] === null ? null : new Date(times[i]!),
543529
hot: i >= bars.length - hotBars,
544530
}));
545531

@@ -627,6 +613,7 @@ export function ReportMetricRow({
627613
series,
628614
windowMinutes,
629615
anomalyMinutes,
616+
seriesEndMs,
630617
formatPoint,
631618
}: {
632619
label: string;
@@ -639,6 +626,7 @@ export function ReportMetricRow({
639626
series?: number[];
640627
windowMinutes: number;
641628
anomalyMinutes?: number;
629+
seriesEndMs: number | null;
642630
formatPoint: (value: number) => string;
643631
}) {
644632
const deltaClass =
@@ -671,6 +659,7 @@ export function ReportMetricRow({
671659
severity={severity}
672660
windowMinutes={windowMinutes}
673661
anomalyMinutes={anomalyMinutes}
662+
seriesEndMs={seriesEndMs}
674663
formatPoint={formatPoint}
675664
label={label}
676665
className={SPARK_CELL_CLASS}

0 commit comments

Comments
 (0)