diff --git a/.changeset/loc-backfill-command-center.md b/.changeset/loc-backfill-command-center.md new file mode 100644 index 0000000000..7a3fa6e85e --- /dev/null +++ b/.changeset/loc-backfill-command-center.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": minor +--- + +Add a Command Center Productivity control for previewing and applying historical LOC backfills from the dashboard. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 2ca0a67218..de1e9379d9 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -806,7 +806,7 @@ Features: - **Tokens** breaks down token totals, estimated cost, tasks, and per-model usage. Per-model and per-provider breakdowns use the task's analytics-only actually-used model snapshot when available, so usage from settings-resolved runs appears under the real runtime model instead of `(unknown)` without changing future model resolution; estimated cost uses the same snapshot-first, legacy-fallback model identity so those resolved runs price normally when the model is in the pricing table. Estimated cost is derived at read time from recorded token counts multiplied by the effective per-model pricing table: Settings → Global Models pricing overrides win first, then the built-in fallback table is used. It is not persisted, so historical rows stay tied to current maintained prices instead of stale stored billing truth. The Tokens area shows a **prices as of** date/source for the effective table, marks pricing older than the staleness threshold as low-confidence, and shows cost unavailable for models with no pricing entry rather than guessing a price. It includes the existing token-usage-over-time chart, an additive recharts multi-series line graph, and a token-share pie backed by the same grouped token analytics; use the granularity control to switch the time-series request between hourly, daily, and weekly buckets. The token total and charts poll on a bounded cadence, keep the previous data visible during refresh, animate decorative count/bar transitions, and disable those animations for reduced-motion users. - **Tools** shows autonomy ratio, tool-call volume, intervention counts, sessions, and tool categories. The area keeps the existing category bar and adds a recharts category-share pie from `ToolAnalytics.byCategory`. There is intentionally no tools line chart yet because `ToolAnalytics` does not expose a per-day tool trend; the dashboard does not fabricate one or call a new endpoint. - **Activity** tracks sessions, messages, active nodes, active agents, agent heartbeat runs, and stickiness. Agent-run sheets show total, active, completed, and failed runs for the selected range, and the Agent runs/day sparkline trends runs by `agentRuns.startedAt`. The area keeps the existing live animated line charts for messages/day, active agents/day, active nodes/day, and combined throughput/day (`messages + active agents + active nodes`), and adds a recharts multi-series line graph for messages, active agents, and agent runs plus an agent-run outcome pie from the existing `agentRuns` split. These charts reuse the existing activity analytics endpoint, refresh on a bounded 15-second cadence while mounted, keep the previous data visible during refreshes, and disable decorative draw-on motion for reduced-motion users. -- **Productivity** separates outcome counters (commits and pull requests), task-duration stats, and volume proxies such as modified files, lines changed, and files by language. The task-duration block counts done tasks completed in the selected range and shows average, median, p90, and total active execution time from `cumulativeActiveMs`; when no qualifying duration data exists, duration values render the unavailable `—` sentinel rather than `0`. It keeps the files-by-language bar and adds a language-share pie from `ProductivityAnalytics.byLanguage`. There is intentionally no productivity line chart because the current productivity response has no per-day throughput or completion time series; no new endpoint is called. +- **Productivity** separates outcome counters (commits and pull requests), task-duration stats, and volume proxies such as modified files, lines changed, and files by language. The task-duration block counts done tasks completed in the selected range and shows average, median, p90, and total active execution time from `cumulativeActiveMs`; when no qualifying duration data exists, duration values render the unavailable `—` sentinel rather than `0`. The Lines changed card includes **Preview LOC backfill**, an explicit operator control for historical commit-association diff stats. Preview runs the project-scoped backfill in dry-run mode by default and reports scanned rows, distinct commits, updated rows, skipped unavailable commits, and skipped invalid SHAs without writing; **Apply backfill** appears after a preview and requires danger confirmation before persisting additions/deletions to `task_commit_associations`, then renders the same counts as an applied report. It keeps the files-by-language bar and adds a language-share pie from `ProductivityAnalytics.byLanguage`. There is intentionally no productivity line chart because the current productivity response has no per-day throughput or completion time series; no new endpoint is called. - **Team** shows the read-only agent org chart, heartbeat pause/resume backed by the existing `enginePaused` setting, a per-agent analytics table, tokens-by-agent and tasks-done-by-agent charts, and a real token-share pie from the same per-agent token totals. The org chart is styled by Command Center's Team CSS, not lazy Agents view CSS, auto-switches to a horizontal top-down tree when the container is wide enough using the same breakpoint resolver as the full Agents view, and otherwise keeps the vertical nested list inside the taller scrollable org-chart container. The org-chart scroll container supports mouse click-and-drag panning while touch devices keep native scrolling. Parent agents draw connector lines to child agents in both horizontal and vertical Team layouts across desktop and mobile breakpoints. Org nodes show only agent names so role/title description/meta text does not clutter Team operations. Metrics come only from the project-scoped `tasks` and `agents` tables: token totals and estimated cost are summed from the `tokenUsage*` columns by `assignedAgentId`, files changed counts parsed `tasks.modifiedFiles` paths, tasks done counts `column = 'done'` moves in the selected range, and in-progress / in-review values reflect current task columns. Agent name, role, and live state come from the `agents` table; deleted-agent task history falls back to the raw agent id instead of crashing. The tab uses `/api/command-center/team`, adds no schema, never calls GitHub, and intentionally leaves per-agent issues filed/fixed to FN-6653. Team has no per-day analytics series today, so it intentionally does not render a line chart or fabricate a trend. Decorative chart reveal motion uses duration tokens and is disabled for reduced-motion users. - **Ecosystem** shows active model breadth, per-model task activity, and real plugin activations for the selected range. Plugin activation counts come from project-scoped plugin/extension load events via `/api/command-center/plugin-activations`; if no activation rows exist in range, the metric renders unavailable (`—`) rather than fabricating zero. The tab still reuses the tokens analytics endpoint grouped by model, adds a task-share-by-model pie from `TokenAnalytics.groups`, and renders a tokens/tasks trend line when `TokenAnalytics.series` buckets are present; if series buckets are absent, no synthetic trend is shown. diff --git a/packages/dashboard/app/components/command-center/CommandCenter.css b/packages/dashboard/app/components/command-center/CommandCenter.css index 4e62e3c985..4f67b19c19 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.css +++ b/packages/dashboard/app/components/command-center/CommandCenter.css @@ -193,15 +193,20 @@ The stat number must render as large as possible while never overflowing the car /* FNXC:CommandCenterGithub 2026-06-18-19:27: The GitHub closed-at backfill control lives inside the existing Fixed by Fusion card and must use tokenized spacing/status colors so the operator result row stays readable on desktop and mobile without creating a separate layout surface. + +FNXC:CommandCenterLocBackfill 2026-06-23-00:00: +The Productivity LOC backfill control reuses the Command Center backfill layout contract: tokenized gaps, semantic error/warning colors, and full-width mobile stacking at the shared 768px breakpoint. */ -.cc-github-backfill-actions { +.cc-github-backfill-actions, +.cc-productivity-backfill-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); } -.cc-github-backfill-status { +.cc-github-backfill-status, +.cc-productivity-backfill-status { display: flex; flex-direction: column; gap: var(--space-xs); @@ -209,21 +214,25 @@ The GitHub closed-at backfill control lives inside the existing Fixed by Fusion font-size: 0.75rem; } -.cc-github-backfill-status--error { +.cc-github-backfill-status--error, +.cc-productivity-backfill-status--error { color: var(--color-error); } -.cc-github-backfill-status--warning { +.cc-github-backfill-status--warning, +.cc-productivity-backfill-status--warning { color: var(--color-warning); } @media (max-width: 768px) { - .cc-github-backfill-actions { + .cc-github-backfill-actions, + .cc-productivity-backfill-actions { align-items: stretch; flex-direction: column; } - .cc-github-backfill-actions .btn { + .cc-github-backfill-actions .btn, + .cc-productivity-backfill-actions .btn { justify-content: center; inline-size: 100%; } diff --git a/packages/dashboard/app/components/command-center/areas/ProductivityArea.tsx b/packages/dashboard/app/components/command-center/areas/ProductivityArea.tsx index 08159db9ce..7d8e4185b9 100644 --- a/packages/dashboard/app/components/command-center/areas/ProductivityArea.tsx +++ b/packages/dashboard/app/components/command-center/areas/ProductivityArea.tsx @@ -1,6 +1,12 @@ -import { useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { RefreshCw } from "lucide-react"; import type { ProductivityAnalytics } from "@fusion/core"; +import { + backfillCommitAssociationDiffStats, + type CommitAssociationDiffBackfillReport, +} from "../../../api/legacy"; +import { useConfirm } from "../../../hooks/useConfirm"; import type { DateRange } from "../DateRangePicker"; import { Bar } from "../charts/Bar"; import { PieChart } from "../charts/recharts"; @@ -37,10 +43,68 @@ ProductivityAnalytics exposes a categorical language distribution but no per-day */ export function ProductivityArea({ range }: { range: DateRange }) { const { t } = useTranslation("app"); + const { confirm } = useConfirm(); const { data, isLoading, error } = useAnalyticsArea( "/command-center/productivity", range, ); + const [isBackfilling, setIsBackfilling] = useState(false); + const [backfillReport, setBackfillReport] = useState(null); + const [backfillError, setBackfillError] = useState(null); + + const handleBackfillPreview = useCallback(async () => { + if (isBackfilling) return; + + setIsBackfilling(true); + setBackfillError(null); + setBackfillReport(null); + + try { + const report = await backfillCommitAssociationDiffStats({ dryRun: true }); + setBackfillReport(report); + } catch (err) { + setBackfillError( + err instanceof Error + ? err.message + : t("commandCenter.productivity.backfillFailed", "Failed to backfill historical LOC stats"), + ); + } finally { + setIsBackfilling(false); + } + }, [isBackfilling, t]); + + const handleApplyBackfill = useCallback(async () => { + if (isBackfilling) return; + + const confirmed = await confirm({ + title: t("commandCenter.productivity.backfillConfirmTitle", "Apply LOC backfill?"), + message: t( + "commandCenter.productivity.backfillConfirmMessage", + "This will persist diff stats to task_commit_associations for historical commit associations. Review the dry-run counts before applying.", + ), + confirmLabel: t("commandCenter.productivity.backfillApply", "Apply backfill"), + cancelLabel: t("common.cancel", "Cancel"), + danger: true, + }); + + if (!confirmed) return; + + setIsBackfilling(true); + setBackfillError(null); + + try { + const report = await backfillCommitAssociationDiffStats({ dryRun: false }); + setBackfillReport(report); + } catch (err) { + setBackfillError( + err instanceof Error + ? err.message + : t("commandCenter.productivity.backfillFailed", "Failed to backfill historical LOC stats"), + ); + } finally { + setIsBackfilling(false); + } + }, [confirm, isBackfilling, t]); const languageBars = useMemo( () => @@ -89,6 +153,11 @@ export function ProductivityArea({ range }: { range: DateRange }) { "commandCenter.productivity.durationUnavailable", "Task duration is unavailable until completed tasks have active execution time recorded", ); + const backfillStatusClass = backfillError + ? "cc-productivity-backfill-status--error" + : isBackfilling + ? "cc-productivity-backfill-status--warning" + : ""; const renderDurationValue = (value: number | null, testId: string) => durationUnavailable || value === null ? ( @@ -161,6 +230,77 @@ export function ProductivityArea({ range }: { range: DateRange }) { )} {t("commandCenter.productivity.volumeHint", "volume, not outcome")} + {/* + FNXC:CommandCenterLocBackfill 2026-06-23-00:00: + Historical LOC repair must be an explicit operator action, not render-time analytics loading. The first action always previews with the API dry-run default, non-dry-run writes are gated by a danger confirmation that names task_commit_associations, and the returned scanned/distinct/updated/skipped counts are surfaced with tokenized desktop/mobile styling while preserving the LOC unavailable sentinel. + */} +
+ + {backfillReport?.dryRun ? ( + + ) : null} +
+ {isBackfilling || backfillReport || backfillError ? ( +
+ {isBackfilling ? ( + {t("commandCenter.productivity.backfillPending", "LOC backfill check is running.")} + ) : null} + {backfillError ? {backfillError} : null} + {backfillReport ? ( + <> + {t("commandCenter.productivity.backfillResult", "Backfill report")} + + {backfillReport.dryRun + ? t("commandCenter.productivity.backfillPreviewLabel", "Dry-run preview") + : t("commandCenter.productivity.backfillAppliedLabel", "Applied")} + + + {t("commandCenter.productivity.backfillScannedRows", "Scanned rows")}: {backfillReport.scannedRows} + + + {t("commandCenter.productivity.backfillDistinctCommits", "Distinct commits")}: {backfillReport.distinctCommits} + + + {t("commandCenter.productivity.backfillUpdatedRows", "Updated rows")}: {backfillReport.updatedRows} + + + {t( + "commandCenter.productivity.backfillSkippedUnavailableCommits", + "Skipped unavailable commits", + )}: {backfillReport.skippedUnavailableCommits} + + + {t("commandCenter.productivity.backfillSkippedInvalidShas", "Skipped invalid SHAs")}: {backfillReport.skippedInvalidShas} + + + ) : null} +
+ ) : null} diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx index e65a28ae10..7c9c899f39 100644 --- a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx +++ b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx @@ -2,6 +2,8 @@ FNXC:CommandCenter 2026-06-16-09:42: Command Center area component tests (PR #1683). Pin loading/error/unavailable-vs-zero rendering for each analytics area against mocked fixtures so the "—" sentinel and cost-unavailable contracts can't regress. */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor, within, act, renderHook } from "@testing-library/react"; import type { OrgTreeNode } from "@fusion/core"; @@ -10,6 +12,7 @@ import type { OrgTreeNode } from "@fusion/core"; const mocks = vi.hoisted(() => ({ api: vi.fn(), backfillGithubSourceIssueClosedAt: vi.fn(), + backfillCommitAssociationDiffStats: vi.fn(), fetchOrgTree: vi.fn(), fetchExecutorStats: vi.fn(), fetchSettings: vi.fn(), @@ -19,6 +22,7 @@ const mocks = vi.hoisted(() => ({ })); const apiMock = mocks.api; const backfillGithubSourceIssueClosedAtMock = mocks.backfillGithubSourceIssueClosedAt; +const backfillCommitAssociationDiffStatsMock = mocks.backfillCommitAssociationDiffStats; const fetchOrgTreeMock = mocks.fetchOrgTree; const fetchExecutorStatsMock = mocks.fetchExecutorStats; const toggleEnginePauseMock = mocks.toggleEnginePause; @@ -27,6 +31,8 @@ vi.mock("../../../../api/legacy", () => ({ api: (path: string, opts?: RequestInit) => mocks.api(path, opts), apiBackfillGithubSourceIssueClosedAt: (options?: { offset?: number; limit?: number }, projectId?: string) => mocks.backfillGithubSourceIssueClosedAt(options, projectId), + backfillCommitAssociationDiffStats: (options?: { dryRun?: boolean }, projectId?: string) => + mocks.backfillCommitAssociationDiffStats(options, projectId), fetchOrgTree: mocks.fetchOrgTree, fetchExecutorStats: mocks.fetchExecutorStats, fetchSettings: mocks.fetchSettings, @@ -50,6 +56,7 @@ import { TeamArea } from "../TeamArea"; import { ActivityArea } from "../ActivityArea"; import { EcosystemArea } from "../EcosystemArea"; import { useAnalyticsArea } from "../useAnalyticsArea"; +import { ConfirmDialogProvider } from "../../../../hooks/useConfirm"; import type { DateRange } from "../DateRangePicker"; const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" }; @@ -135,6 +142,27 @@ function githubFixture() { }; } +function productivityFixture() { + return { + from: "2026-06-08", + to: null, + modifiedFiles: 7, + commits: 4, + pullRequests: 2, + byLanguage: [{ language: "TypeScript", count: 7 }], + loc: { value: null, unavailable: true }, + hoursSaved: { value: null, unavailable: true }, + taskDuration: { + completedTasks: 3, + averageMs: 90 * 60 * 1000, + medianMs: 60 * 60 * 1000, + p90Ms: 2 * 60 * 60 * 1000, + totalMs: 270 * 60 * 1000, + unavailable: false, + }, + }; +} + function installElementClientWidth(width: number) { return vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(width); } @@ -250,6 +278,7 @@ function activityFixture() { beforeEach(() => { apiMock.mockReset(); backfillGithubSourceIssueClosedAtMock.mockReset(); + backfillCommitAssociationDiffStatsMock.mockReset(); fetchOrgTreeMock.mockReset(); fetchOrgTreeMock.mockResolvedValue([]); fetchExecutorStatsMock.mockReset(); @@ -870,6 +899,14 @@ describe("ToolsArea", () => { }); describe("ProductivityArea", () => { + function renderProductivityWithConfirm() { + return render( + + + , + ); + } + it("renders unavailable LOC and hours saved as dash sentinels, duration stats, and finite chart geometry", async () => { apiMock.mockResolvedValue({ from: "2026-06-08", @@ -1031,6 +1068,169 @@ describe("ProductivityArea", () => { expect(screen.getByTestId("cc-productivity-pie").textContent).not.toContain("NaN"); expect(screen.getByTestId("cc-productivity-pie").textContent).not.toContain("Infinity"); }); + + it("previews LOC backfill with dry-run counts and preserves the LOC sentinel", async () => { + apiMock.mockResolvedValue(productivityFixture()); + backfillCommitAssociationDiffStatsMock.mockResolvedValueOnce({ + scannedRows: 6, + distinctCommits: 4, + updatedRows: 3, + skippedUnavailableCommits: 2, + skippedInvalidShas: 1, + dryRun: true, + }); + + renderProductivityWithConfirm(); + await screen.findByTestId("cc-area-productivity"); + expect(screen.queryByTestId("cc-productivity-backfill-apply-button")).toBeNull(); + expect(screen.getByTestId("cc-productivity-loc-unavailable").textContent).toBe("—"); + + fireEvent.click(screen.getByTestId("cc-productivity-backfill-button")); + + await waitFor(() => expect(backfillCommitAssociationDiffStatsMock).toHaveBeenCalledWith({ dryRun: true }, undefined)); + const result = await screen.findByTestId("cc-productivity-backfill-result"); + expect(result.textContent).toContain("Dry-run preview"); + expect(result.textContent).toContain("Scanned rows: 6"); + expect(result.textContent).toContain("Distinct commits: 4"); + expect(result.textContent).toContain("Updated rows: 3"); + expect(result.textContent).toContain("Skipped unavailable commits: 2"); + expect(result.textContent).toContain("Skipped invalid SHAs: 1"); + expect(screen.getByTestId("cc-productivity-backfill-apply-button")).toBeTruthy(); + expect(screen.getByTestId("cc-productivity-loc-unavailable").textContent).toBe("—"); + }); + + it("requires confirmation before applying the LOC backfill and aborts cleanly on cancel", async () => { + apiMock.mockResolvedValue(productivityFixture()); + backfillCommitAssociationDiffStatsMock.mockResolvedValueOnce({ + scannedRows: 2, + distinctCommits: 2, + updatedRows: 2, + skippedUnavailableCommits: 0, + skippedInvalidShas: 0, + dryRun: true, + }); + + renderProductivityWithConfirm(); + await screen.findByTestId("cc-area-productivity"); + fireEvent.click(screen.getByTestId("cc-productivity-backfill-button")); + await screen.findByTestId("cc-productivity-backfill-apply-button"); + + fireEvent.click(screen.getByTestId("cc-productivity-backfill-apply-button")); + const dialog = await screen.findByRole("dialog", { name: "Apply LOC backfill?" }); + expect(dialog.textContent).toContain("task_commit_associations"); + fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); + + await waitFor(() => expect(screen.queryByRole("dialog", { name: "Apply LOC backfill?" })).toBeNull()); + expect(backfillCommitAssociationDiffStatsMock).toHaveBeenCalledTimes(1); + }); + + it("applies the LOC backfill only after the danger confirmation resolves", async () => { + apiMock.mockResolvedValue(productivityFixture()); + backfillCommitAssociationDiffStatsMock + .mockResolvedValueOnce({ + scannedRows: 5, + distinctCommits: 4, + updatedRows: 3, + skippedUnavailableCommits: 1, + skippedInvalidShas: 0, + dryRun: true, + }) + .mockResolvedValueOnce({ + scannedRows: 5, + distinctCommits: 4, + updatedRows: 3, + skippedUnavailableCommits: 1, + skippedInvalidShas: 0, + dryRun: false, + }); + + renderProductivityWithConfirm(); + await screen.findByTestId("cc-area-productivity"); + fireEvent.click(screen.getByTestId("cc-productivity-backfill-button")); + await screen.findByText("Dry-run preview"); + + fireEvent.click(screen.getByTestId("cc-productivity-backfill-apply-button")); + const dialog = await screen.findByRole("dialog", { name: "Apply LOC backfill?" }); + fireEvent.click(within(dialog).getByRole("button", { name: "Apply backfill" })); + + await waitFor(() => expect(backfillCommitAssociationDiffStatsMock).toHaveBeenNthCalledWith(2, { dryRun: false }, undefined)); + const result = await screen.findByTestId("cc-productivity-backfill-result"); + expect(result.textContent).toContain("Applied"); + expect(result.textContent).toContain("Updated rows: 3"); + }); + + it("disables the preview button and shows pending status while LOC backfill is in flight", async () => { + apiMock.mockResolvedValue(productivityFixture()); + let resolveBackfill: ((value: { scannedRows: number; distinctCommits: number; updatedRows: number; skippedUnavailableCommits: number; skippedInvalidShas: number; dryRun: boolean }) => void) | null = null; + backfillCommitAssociationDiffStatsMock.mockImplementationOnce( + () => new Promise((resolve) => { + resolveBackfill = resolve; + }), + ); + + renderProductivityWithConfirm(); + await screen.findByTestId("cc-area-productivity"); + const button = screen.getByTestId("cc-productivity-backfill-button") as HTMLButtonElement; + fireEvent.click(button); + + await waitFor(() => expect(button.disabled).toBe(true)); + expect(screen.getByTestId("cc-productivity-backfill-result").textContent).toContain("LOC backfill check is running."); + expect(screen.getByTestId("cc-productivity-backfill-result").className).toContain("cc-productivity-backfill-status--warning"); + fireEvent.click(button); + expect(backfillCommitAssociationDiffStatsMock).toHaveBeenCalledTimes(1); + + resolveBackfill?.({ + scannedRows: 1, + distinctCommits: 1, + updatedRows: 1, + skippedUnavailableCommits: 0, + skippedInvalidShas: 0, + dryRun: true, + }); + await screen.findByText("Dry-run preview"); + }); + + it("renders endpoint errors with the tokenized error status", async () => { + apiMock.mockResolvedValue(productivityFixture()); + backfillCommitAssociationDiffStatsMock.mockRejectedValueOnce(new Error("loc endpoint failed")); + + renderProductivityWithConfirm(); + await screen.findByTestId("cc-area-productivity"); + fireEvent.click(screen.getByTestId("cc-productivity-backfill-button")); + + const result = await screen.findByTestId("cc-productivity-backfill-result"); + expect(result.textContent).toContain("loc endpoint failed"); + expect(result.className).toContain("cc-productivity-backfill-status--error"); + }); + + it("renders an all-zero LOC backfill report truthfully", async () => { + apiMock.mockResolvedValue(productivityFixture()); + backfillCommitAssociationDiffStatsMock.mockResolvedValueOnce({ + scannedRows: 0, + distinctCommits: 0, + updatedRows: 0, + skippedUnavailableCommits: 0, + skippedInvalidShas: 0, + dryRun: true, + }); + + renderProductivityWithConfirm(); + await screen.findByTestId("cc-area-productivity"); + fireEvent.click(screen.getByTestId("cc-productivity-backfill-button")); + + const result = await screen.findByTestId("cc-productivity-backfill-result"); + expect(result.textContent).toContain("Scanned rows: 0"); + expect(result.textContent).toContain("Distinct commits: 0"); + expect(result.textContent).toContain("Updated rows: 0"); + expect(result.textContent).toContain("Skipped unavailable commits: 0"); + expect(result.textContent).toContain("Skipped invalid SHAs: 0"); + }); + + it("keeps LOC backfill mobile actions stacked and full width in CSS", () => { + const css = readFileSync(join(process.cwd(), "app/components/command-center/CommandCenter.css"), "utf8"); + expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*\.cc-productivity-backfill-actions[\s\S]*flex-direction: column;/); + expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*\.cc-productivity-backfill-actions \.btn[\s\S]*inline-size: 100%;/); + }); }); describe("TeamArea", () => { diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index f7cff4a50b..5cebf1ee11 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -1673,6 +1673,21 @@ }, "productivity": { "averageDuration": "Average", + "backfillAppliedLabel": "Applied", + "backfillApply": "Apply backfill", + "backfillBusy": "Checking historical LOC…", + "backfillButton": "Preview LOC backfill", + "backfillConfirmMessage": "This will persist diff stats to task_commit_associations for historical commit associations. Review the dry-run counts before applying.", + "backfillConfirmTitle": "Apply LOC backfill?", + "backfillDistinctCommits": "Distinct commits", + "backfillFailed": "Failed to backfill historical LOC stats", + "backfillPending": "LOC backfill check is running.", + "backfillPreviewLabel": "Dry-run preview", + "backfillResult": "Backfill report", + "backfillScannedRows": "Scanned rows", + "backfillSkippedInvalidShas": "Skipped invalid SHAs", + "backfillSkippedUnavailableCommits": "Skipped unavailable commits", + "backfillUpdatedRows": "Updated rows", "byLanguage": "Files by language", "commits": "Commits", "completedTasks": "Completed tasks", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 16c2c188c5..106b72a4b6 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -1673,6 +1673,21 @@ }, "productivity": { "averageDuration": "", + "backfillAppliedLabel": "", + "backfillApply": "", + "backfillBusy": "", + "backfillButton": "", + "backfillConfirmMessage": "", + "backfillConfirmTitle": "", + "backfillDistinctCommits": "", + "backfillFailed": "", + "backfillPending": "", + "backfillPreviewLabel": "", + "backfillResult": "", + "backfillScannedRows": "", + "backfillSkippedInvalidShas": "", + "backfillSkippedUnavailableCommits": "", + "backfillUpdatedRows": "", "byLanguage": "", "commits": "", "completedTasks": "", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index ed6973fddd..852cdc3b16 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -1673,6 +1673,21 @@ }, "productivity": { "averageDuration": "", + "backfillAppliedLabel": "", + "backfillApply": "", + "backfillBusy": "", + "backfillButton": "", + "backfillConfirmMessage": "", + "backfillConfirmTitle": "", + "backfillDistinctCommits": "", + "backfillFailed": "", + "backfillPending": "", + "backfillPreviewLabel": "", + "backfillResult": "", + "backfillScannedRows": "", + "backfillSkippedInvalidShas": "", + "backfillSkippedUnavailableCommits": "", + "backfillUpdatedRows": "", "byLanguage": "", "commits": "", "completedTasks": "", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index de01a82ff5..5440065e2d 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -1673,6 +1673,21 @@ }, "productivity": { "averageDuration": "", + "backfillAppliedLabel": "", + "backfillApply": "", + "backfillBusy": "", + "backfillButton": "", + "backfillConfirmMessage": "", + "backfillConfirmTitle": "", + "backfillDistinctCommits": "", + "backfillFailed": "", + "backfillPending": "", + "backfillPreviewLabel": "", + "backfillResult": "", + "backfillScannedRows": "", + "backfillSkippedInvalidShas": "", + "backfillSkippedUnavailableCommits": "", + "backfillUpdatedRows": "", "byLanguage": "", "commits": "", "completedTasks": "", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 785e1516f0..ff3fded2af 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -1673,6 +1673,21 @@ }, "productivity": { "averageDuration": "", + "backfillAppliedLabel": "", + "backfillApply": "", + "backfillBusy": "", + "backfillButton": "", + "backfillConfirmMessage": "", + "backfillConfirmTitle": "", + "backfillDistinctCommits": "", + "backfillFailed": "", + "backfillPending": "", + "backfillPreviewLabel": "", + "backfillResult": "", + "backfillScannedRows": "", + "backfillSkippedInvalidShas": "", + "backfillSkippedUnavailableCommits": "", + "backfillUpdatedRows": "", "byLanguage": "", "commits": "", "completedTasks": "", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 71c98e86d3..6f0896779f 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -1673,6 +1673,21 @@ }, "productivity": { "averageDuration": "", + "backfillAppliedLabel": "", + "backfillApply": "", + "backfillBusy": "", + "backfillButton": "", + "backfillConfirmMessage": "", + "backfillConfirmTitle": "", + "backfillDistinctCommits": "", + "backfillFailed": "", + "backfillPending": "", + "backfillPreviewLabel": "", + "backfillResult": "", + "backfillScannedRows": "", + "backfillSkippedInvalidShas": "", + "backfillSkippedUnavailableCommits": "", + "backfillUpdatedRows": "", "byLanguage": "", "commits": "", "completedTasks": "", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index aecba020e6..840a9c4675 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -1584,7 +1584,7 @@ export default interface Resources { "trendTitle": "Ecosystem trend", "uniqueModels": "Active models" }, - "empty": "No usage data yet. Run some agents to populate the Command Center.", + "empty": "No usage data yet. Run some agents to populate the Dashboard.", "funnel": { "ariaLabel": "Tasks per workflow stage", "completionRate": "Completion rate", @@ -1630,7 +1630,7 @@ export default interface Resources { "totalsTitle": "GitHub issue flow" }, "heading": "Dashboard", - "loading": "Loading command center...", + "loading": "Loading dashboard...", "missionControl": { "activeNodes": "Active nodes", "activeRuns": "Active runs", @@ -1675,6 +1675,21 @@ export default interface Resources { }, "productivity": { "averageDuration": "Average", + "backfillAppliedLabel": "Applied", + "backfillApply": "Apply backfill", + "backfillBusy": "Checking historical LOC…", + "backfillButton": "Preview LOC backfill", + "backfillConfirmMessage": "This will persist diff stats to task_commit_associations for historical commit associations. Review the dry-run counts before applying.", + "backfillConfirmTitle": "Apply LOC backfill?", + "backfillDistinctCommits": "Distinct commits", + "backfillFailed": "Failed to backfill historical LOC stats", + "backfillPending": "LOC backfill check is running.", + "backfillPreviewLabel": "Dry-run preview", + "backfillResult": "Backfill report", + "backfillScannedRows": "Scanned rows", + "backfillSkippedInvalidShas": "Skipped invalid SHAs", + "backfillSkippedUnavailableCommits": "Skipped unavailable commits", + "backfillUpdatedRows": "Updated rows", "byLanguage": "Files by language", "commits": "Commits", "completedTasks": "Completed tasks", @@ -1918,6 +1933,7 @@ export default interface Resources { "initializingDashboard": "Initializing dashboard...", "loadingMessage": "Loading Fusion dashboard", "loadingProgress": "Dashboard loading progress", + "title": "Project Dashboard", "updatingMessage": "Updating Fusion dashboard", "updatingVersion": "Updating to a new frontend version..." }, @@ -2317,6 +2333,7 @@ export default interface Resources { "stateIdle": "Idle", "statePaused": "Paused", "stateRunning": "Running", + "stateStopped": "Stopped", "status": "Executor status", "stuck": "Stuck", "temporary": "Temporary", @@ -3231,7 +3248,7 @@ export default interface Resources { "unpauseSelectedTitle": "Unpause selected tasks that are currently paused", "unpauseUnavailable": "Unpause action is unavailable", "useProjectDefault": "Use project default", - "viewOptions": "View options", + "viewOptions": "View", "workflowLabel": "Workflow" }, "mailbox": { @@ -8213,7 +8230,7 @@ export default interface Resources { "agent": "Column agent", "agentBadgeDefer": "Column agent (defer)", "agentBadgeOverride": "Column agent (override)", - "agentFlagHint": "Enable both experimentalFeatures.workflowColumns and experimentalFeatures.workflowGraphExecutor to staff columns with agents", + "agentFlagHint": "Workflow columns are available by default", "agentLabel": "Column agent", "agentMode": "Agent mode", "agentModeDefer": "Defer", @@ -8496,11 +8513,13 @@ export default interface Resources { "widgetDefault": "Default" }, "workflowSwitcher": { - "countsAria": "{{todoLabel}}: {{todo}}, {{inProgressLabel}}: {{inProgress}}, {{doneLabel}}: {{done}}", + "countsAria": "{{todoLabel}}: {{todo}}, {{inProgressLabel}}: {{inProgress}}, {{doneLabel}}: {{done}}{{mergingSuffix}}", "done": "Done", "editWorkflow": "Edit workflow", "inProgress": "In Progress", "label": "Workflow", + "merging": "Merging", + "mergingTitle": "{{count}} merging task{{plural}}", "newWorkflow": "New workflow", "todo": "Todo", "triggerAria": "Select workflow. Current workflow: {{name}}"