mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat: git hygiene (branch/preview reaping + cleanup sweep) and panel charts; work sessions under Git (#548)
* feat(panel): session-start, 7d overview spend, and 30d business spend charts * feat(panel): surface work sessions as a Git page tab (route was orphaned) * feat(git): reap spent task branches and render previews at lifecycle chokepoints; guarded stale-branch sweep * feat(panel): stale-branch cleanup button on the Git page * fix(git,panel): cursor-resumable sweep, force-delete spent refs, local filter state * docs(map,rag): branch/preview reaping, cleanup sweep, git-tab work sessions, wave-2 charts --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,13 @@ vi.mock("@/hooks/use-tasks", () => ({
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/hooks/use-usage", () => ({
|
||||
useUsageTimeSeries: () => ({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../team-health-cards", () => ({
|
||||
TeamHealthCards: () => <div>TeamHealthCardsStub</div>,
|
||||
@@ -87,6 +94,9 @@ vi.mock("../usage-overview-panel", () => ({
|
||||
vi.mock("../scorecard-overview-panel", () => ({
|
||||
ScorecardOverviewPanel: () => <div>ScorecardOverviewPanelStub</div>,
|
||||
}));
|
||||
vi.mock("../cost-trend-chart", () => ({
|
||||
CostTrendChart: () => <div>CostTrendChartStub</div>,
|
||||
}));
|
||||
|
||||
import { CommandCenter } from "../command-center";
|
||||
|
||||
@@ -106,6 +116,7 @@ describe("CommandCenter", () => {
|
||||
"AuditorAlertsPanelStub",
|
||||
"UsageOverviewPanelStub",
|
||||
"ScorecardOverviewPanelStub",
|
||||
"CostTrendChartStub",
|
||||
"TeamHealthCardsStub",
|
||||
"CeoApprovalQueueStub",
|
||||
"StrategySignalsPanelStub",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { CostTrendChart } from "../cost-trend-chart";
|
||||
import type { UsageTimePoint } from "@/types";
|
||||
|
||||
function buildPoint(overrides: Partial<UsageTimePoint> = {}): UsageTimePoint {
|
||||
return {
|
||||
bucket: new Date().toISOString(),
|
||||
tokens_input: 1000,
|
||||
tokens_output: 500,
|
||||
total_tokens: 1500,
|
||||
cost_usd: 1.23,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("CostTrendChart", () => {
|
||||
it("renders the card title", () => {
|
||||
render(<CostTrendChart data={[buildPoint()]} isLoading={false} />);
|
||||
expect(screen.getByText("Spend Trend (7d)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows an empty state when there is no data", () => {
|
||||
render(<CostTrendChart data={[]} isLoading={false} />);
|
||||
expect(screen.getByText("No usage data")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows an empty state when data is undefined", () => {
|
||||
render(<CostTrendChart data={undefined} isLoading={false} />);
|
||||
expect(screen.getByText("No usage data")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show the empty state while loading", () => {
|
||||
render(<CostTrendChart data={[]} isLoading />);
|
||||
expect(screen.queryByText("No usage data")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useRecentActivity,
|
||||
} from "@/hooks/use-dashboard";
|
||||
import { useTasks } from "@/hooks/use-tasks";
|
||||
import { useUsageTimeSeries } from "@/hooks/use-usage";
|
||||
import { usePageRefresh } from "@/hooks";
|
||||
import { TeamHealthCards } from "./team-health-cards";
|
||||
import { KeyMetricsPanel } from "./key-metrics-panel";
|
||||
@@ -25,6 +26,7 @@ import type { Activity } from "./activity-item";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UsageOverviewPanel } from "./usage-overview-panel";
|
||||
import { ScorecardOverviewPanel } from "./scorecard-overview-panel";
|
||||
import { CostTrendChart } from "./cost-trend-chart";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -62,6 +64,11 @@ export function CommandCenter() {
|
||||
isError: errorActivity,
|
||||
refetch: refetchActivity,
|
||||
} = useRecentActivity(24);
|
||||
const {
|
||||
data: costTrend,
|
||||
isLoading: loadingCostTrend,
|
||||
refetch: refetchCostTrend,
|
||||
} = useUsageTimeSeries("7d");
|
||||
|
||||
const { register, unregister } = usePageRefresh();
|
||||
|
||||
@@ -79,6 +86,9 @@ export function CommandCenter() {
|
||||
() => {
|
||||
void refetchActivity();
|
||||
},
|
||||
() => {
|
||||
void refetchCostTrend();
|
||||
},
|
||||
];
|
||||
callbacks.forEach((cb) => register(cb));
|
||||
return () => {
|
||||
@@ -91,6 +101,7 @@ export function CommandCenter() {
|
||||
refetchFlags,
|
||||
refetchTasks,
|
||||
refetchActivity,
|
||||
refetchCostTrend,
|
||||
]);
|
||||
|
||||
const hasError = errorOverview || errorFlags || errorTasks || errorActivity;
|
||||
@@ -156,6 +167,8 @@ export function CommandCenter() {
|
||||
<ScorecardOverviewPanel />
|
||||
</div>
|
||||
|
||||
<CostTrendChart data={costTrend} isLoading={loadingCostTrend} />
|
||||
|
||||
{/* Section 2: Team Health (team cards + Task Intake + Secretary) */}
|
||||
<section>
|
||||
<HelpTip label="Per-team blocked ratio and throughput, plus the on-demand agents">
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import type { UsageTimePoint } from "@/types";
|
||||
|
||||
interface CostTrendChartProps {
|
||||
data: UsageTimePoint[] | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function formatBucket(bucket: string): string {
|
||||
const d = new Date(bucket);
|
||||
return d.getMonth() + 1 + "/" + d.getDate();
|
||||
}
|
||||
|
||||
function fmtCost(n: number): string {
|
||||
return "$" + n.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact daily spend trend for the Command Center landing page — reuses
|
||||
* GET /usage/time-series (period=7d, daily buckets) so the CEO sees where
|
||||
* the current-period totals in UsageOverviewPanel came from at a glance.
|
||||
*/
|
||||
export function CostTrendChart({ data, isLoading }: CostTrendChartProps) {
|
||||
const chartData = (data ?? []).map((p) => ({
|
||||
day: formatBucket(p.bucket),
|
||||
Cost: p.cost_usd,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<HelpTip label="Provider-priced agent-session cost per day over the last 7 days">
|
||||
<CardTitle className="text-base">Spend Trend (7d)</CardTitle>
|
||||
</HelpTip>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-40 w-full" />
|
||||
) : chartData.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
No usage data
|
||||
</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 4, right: 8, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="fillCost" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--chart-3)"
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--chart-3)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" className="opacity-20" />
|
||||
<XAxis
|
||||
dataKey="day"
|
||||
tick={{ fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={fmtCost}
|
||||
tick={{ fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={44}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => [
|
||||
fmtCost(typeof value === "number" ? value : 0),
|
||||
"Cost",
|
||||
]}
|
||||
contentStyle={{ fontSize: 12 }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="Cost"
|
||||
stroke="var(--chart-3)"
|
||||
fill="url(#fillCost)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user