Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
carry ~5,930 keys each across common/app/errors/cli namespaces;
CLI bundles regenerated (6 locales incl. ko)
Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
literal {{placeholders}}); dashboard vitest.setup boots a minimal en
i18next instance for the same reason
Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
4.7 KiB
TypeScript
125 lines
4.7 KiB
TypeScript
import { Loader2 } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { consumeVersionUpdateFlag } from "../versionCheck";
|
|
import { SWR_CACHE_KEYS, clearCache } from "../utils/swrCache";
|
|
import "./DashboardLoader.css";
|
|
|
|
export type DashboardLoaderStage = "projects" | "project" | "tasks" | "ready";
|
|
|
|
interface DashboardLoaderProps {
|
|
stage: DashboardLoaderStage;
|
|
}
|
|
|
|
interface LoaderStep {
|
|
id: Exclude<DashboardLoaderStage, "ready">;
|
|
label: string;
|
|
}
|
|
|
|
const LOADER_STEPS: LoaderStep[] = [
|
|
{ id: "projects", label: "dashboard.loaderSteps.projects" },
|
|
{ id: "project", label: "dashboard.loaderSteps.project" },
|
|
{ id: "tasks", label: "dashboard.loaderSteps.tasks" },
|
|
];
|
|
|
|
function getStepState(stepId: LoaderStep["id"], stage: DashboardLoaderStage): "done" | "active" | "pending" {
|
|
if (stage === "ready") {
|
|
return "done";
|
|
}
|
|
|
|
const currentStageIndex = LOADER_STEPS.findIndex((step) => step.id === stage);
|
|
const stepIndex = LOADER_STEPS.findIndex((step) => step.id === stepId);
|
|
|
|
if (stepIndex < currentStageIndex) {
|
|
return "done";
|
|
}
|
|
|
|
if (stepIndex === currentStageIndex) {
|
|
return "active";
|
|
}
|
|
|
|
return "pending";
|
|
}
|
|
|
|
export function DashboardLoader({ stage }: DashboardLoaderProps) {
|
|
const { t } = useTranslation("app");
|
|
const [isVersionUpdate] = useState(() => {
|
|
const versionUpdated = consumeVersionUpdateFlag();
|
|
if (versionUpdated) {
|
|
clearCache(SWR_CACHE_KEYS.TASKS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.PROJECTS);
|
|
clearCache(SWR_CACHE_KEYS.CURRENT_PROJECT_ID);
|
|
clearCache(SWR_CACHE_KEYS.AGENTS);
|
|
clearCache(SWR_CACHE_KEYS.AGENT_STATS);
|
|
clearCache(SWR_CACHE_KEYS.DOCUMENTS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.TODO_LISTS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.CHAT_ROOMS);
|
|
clearCache(SWR_CACHE_KEYS.ACTIVE_CHAT_ROOM_ID);
|
|
clearCache(SWR_CACHE_KEYS.CHAT_SESSIONS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.CHAT_MESSAGES_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.CHAT_AGENTS_MAP_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.MODELS);
|
|
clearCache(SWR_CACHE_KEYS.DISCOVERED_SKILLS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.INSIGHTS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.INSIGHT_LATEST_RUN_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.RESEARCH_SELECTED_ID_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.EVALS_RUNS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.MISSIONS_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.MISSIONS_SELECTED_ID_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.MAILBOX_OUTBOX_PREFIX);
|
|
clearCache(SWR_CACHE_KEYS.MAILBOX_UNREAD_COUNT_PREFIX);
|
|
}
|
|
return versionUpdated;
|
|
});
|
|
|
|
return (
|
|
<div
|
|
className="dashboard-loader"
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-label={isVersionUpdate ? t("dashboard.updatingMessage", "Updating Fusion dashboard") : t("dashboard.loadingMessage", "Loading Fusion dashboard")}
|
|
data-stage={stage}
|
|
data-version-update={isVersionUpdate ? "true" : undefined}
|
|
>
|
|
<div className="dashboard-loader__content">
|
|
<h1 className="dashboard-loader__logo">Fusion</h1>
|
|
{isVersionUpdate ? (
|
|
<p className="dashboard-loader__message dashboard-loader__message--update">
|
|
{t("dashboard.updatingVersion", "Updating to a new frontend version...")}
|
|
</p>
|
|
) : (
|
|
<p className="dashboard-loader__message">{t("dashboard.initializingDashboard", "Initializing dashboard...")}</p>
|
|
)}
|
|
|
|
<ol className="dashboard-loader__steps" aria-label={t("dashboard.loadingProgress", "Dashboard loading progress")}>
|
|
{LOADER_STEPS.map((step) => {
|
|
const stepState = getStepState(step.id, stage);
|
|
|
|
return (
|
|
<li
|
|
key={step.id}
|
|
className={`dashboard-loader__step dashboard-loader__step--${stepState}`}
|
|
data-testid={`dashboard-loader-step-${step.id}`}
|
|
>
|
|
<span className="dashboard-loader__step-icon" aria-hidden="true">
|
|
{stepState === "done" ? (
|
|
"✓"
|
|
) : stepState === "active" ? (
|
|
<Loader2 className="dashboard-loader__spinner animate-spin" size={14} />
|
|
) : (
|
|
"•"
|
|
)}
|
|
</span>
|
|
<span className="dashboard-loader__step-label">{t(step.label, step.label === "dashboard.loaderSteps.projects" ? "Loading projects" : step.label === "dashboard.loaderSteps.project" ? "Selecting project" : "Fetching tasks")}</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|