fix(web): fall back to project totals when dashboard metrics are zero (#2847)
The dashboard reads elapsed time, total cost, and tokens used exclusively from AutoDashboardData. When auto-mode is not active (e.g. manual /gsd next), auto is null and all three metrics show 0 — even though the status bar displays real values via /api/visualizer. Add the same projectTotals polling pattern (30s interval via /api/visualizer) that status-bar.tsx already uses, and wire it into the fallback chain: projectTotals ?? auto ?? 0. Closes #2709
This commit is contained in:
parent
6918fb76c6
commit
7c5dae0298
2 changed files with 109 additions and 3 deletions
|
|
@ -1,5 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState, useCallback } from "react"
|
||||
import {
|
||||
Activity,
|
||||
Clock,
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
useGSDWorkspaceState,
|
||||
useGSDWorkspaceActions,
|
||||
buildPromptCommand,
|
||||
buildProjectUrl,
|
||||
formatDuration,
|
||||
formatCost,
|
||||
formatTokens,
|
||||
|
|
@ -37,6 +39,8 @@ import {
|
|||
} from "@/components/gsd/loading-skeletons"
|
||||
import { ScopeBadge } from "@/components/gsd/scope-badge"
|
||||
import { ProjectWelcome } from "@/components/gsd/project-welcome"
|
||||
import { authFetch } from "@/lib/auth"
|
||||
import { type ProjectTotals } from "@/lib/visualizer-types"
|
||||
|
||||
/** Interpolate progress bar color from red (0%) through yellow (50%) to green (100%) using oklch. */
|
||||
function getProgressColor(percent: number): string {
|
||||
|
|
@ -114,10 +118,40 @@ export function Dashboard({ onSwitchView, onExpandTerminal }: DashboardProps = {
|
|||
const auto = getLiveAutoDashboard(state)
|
||||
const bridge = boot?.bridge ?? null
|
||||
const freshness = state.live.freshness
|
||||
const projectCwd = boot?.project.cwd
|
||||
|
||||
const elapsed = auto?.elapsed ?? 0
|
||||
const totalCost = auto?.totalCost ?? 0
|
||||
const totalTokens = auto?.totalTokens ?? 0
|
||||
// ── Project-level totals from visualizer API ──
|
||||
// Provides fallback metrics when auto-mode is not active (#2709).
|
||||
// Same polling pattern as status-bar.tsx.
|
||||
const [projectTotals, setProjectTotals] = useState<ProjectTotals | null>(null)
|
||||
|
||||
const fetchProjectTotals = useCallback(async () => {
|
||||
try {
|
||||
const resp = await authFetch(buildProjectUrl("/api/visualizer", projectCwd))
|
||||
if (!resp.ok) return
|
||||
const json = await resp.json()
|
||||
if (json.totals) setProjectTotals(json.totals)
|
||||
} catch {
|
||||
// Silently ignore — dashboard metrics are non-critical
|
||||
}
|
||||
}, [projectCwd])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
void fetchProjectTotals()
|
||||
}, 0)
|
||||
const interval = window.setInterval(() => {
|
||||
void fetchProjectTotals()
|
||||
}, 30_000)
|
||||
return () => {
|
||||
window.clearTimeout(timeout)
|
||||
window.clearInterval(interval)
|
||||
}
|
||||
}, [fetchProjectTotals])
|
||||
|
||||
const elapsed = projectTotals?.duration ?? auto?.elapsed ?? 0
|
||||
const totalCost = projectTotals?.cost ?? auto?.totalCost ?? 0
|
||||
const totalTokens = projectTotals?.tokens.total ?? auto?.totalTokens ?? 0
|
||||
const rtkSavings = auto?.rtkSavings ?? null
|
||||
const rtkEnabled = auto?.rtkEnabled === true
|
||||
|
||||
|
|
|
|||
72
web/lib/__tests__/dashboard-metrics-fallback.test.ts
Normal file
72
web/lib/__tests__/dashboard-metrics-fallback.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/**
|
||||
* Regression tests for dashboard metric fallback chain.
|
||||
*
|
||||
* The dashboard reads metrics from two sources:
|
||||
* 1. projectTotals (polled from /api/visualizer — always available)
|
||||
* 2. auto (live auto-mode data — null when auto is not active)
|
||||
*
|
||||
* Fallback chain: projectTotals?.X ?? auto?.X ?? 0
|
||||
*
|
||||
* See: https://github.com/gsd-build/gsd-2/issues/2709
|
||||
*/
|
||||
|
||||
interface ProjectTotals {
|
||||
duration: number;
|
||||
cost: number;
|
||||
tokens: { total: number };
|
||||
}
|
||||
|
||||
interface AutoDashboard {
|
||||
elapsed: number;
|
||||
totalCost: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
/** Mirrors the fallback logic in dashboard.tsx */
|
||||
function deriveMetrics(
|
||||
projectTotals: ProjectTotals | null,
|
||||
auto: AutoDashboard | null,
|
||||
) {
|
||||
return {
|
||||
elapsed: projectTotals?.duration ?? auto?.elapsed ?? 0,
|
||||
totalCost: projectTotals?.cost ?? auto?.totalCost ?? 0,
|
||||
totalTokens: projectTotals?.tokens.total ?? auto?.totalTokens ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe("dashboard metric fallback (#2709 regression)", () => {
|
||||
test("returns zero when both sources are null", () => {
|
||||
const result = deriveMetrics(null, null);
|
||||
assert.equal(result.elapsed, 0);
|
||||
assert.equal(result.totalCost, 0);
|
||||
assert.equal(result.totalTokens, 0);
|
||||
});
|
||||
|
||||
test("uses auto data when projectTotals is null", () => {
|
||||
const auto: AutoDashboard = { elapsed: 5000, totalCost: 1.5, totalTokens: 10000 };
|
||||
const result = deriveMetrics(null, auto);
|
||||
assert.equal(result.elapsed, 5000);
|
||||
assert.equal(result.totalCost, 1.5);
|
||||
assert.equal(result.totalTokens, 10000);
|
||||
});
|
||||
|
||||
test("uses projectTotals when auto is null (manual mode)", () => {
|
||||
const totals: ProjectTotals = { duration: 60000, cost: 3.2, tokens: { total: 50000 } };
|
||||
const result = deriveMetrics(totals, null);
|
||||
assert.equal(result.elapsed, 60000);
|
||||
assert.equal(result.totalCost, 3.2);
|
||||
assert.equal(result.totalTokens, 50000);
|
||||
});
|
||||
|
||||
test("projectTotals takes precedence over auto when both present", () => {
|
||||
const totals: ProjectTotals = { duration: 120000, cost: 5.0, tokens: { total: 80000 } };
|
||||
const auto: AutoDashboard = { elapsed: 10000, totalCost: 0.5, totalTokens: 5000 };
|
||||
const result = deriveMetrics(totals, auto);
|
||||
assert.equal(result.elapsed, 120000, "projectTotals duration should take precedence");
|
||||
assert.equal(result.totalCost, 5.0, "projectTotals cost should take precedence");
|
||||
assert.equal(result.totalTokens, 80000, "projectTotals tokens should take precedence");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue