Files
fusion/packages/dashboard/app/hooks/useMeshState.ts
gsxdsm 1e49494bac feat(i18n): full-sweep string migration — 5,930 keys across 5 locales (#1352)
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>
2026-06-03 19:06:53 -07:00

108 lines
3.1 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import type { NodeMeshState } from "@fusion/core";
import { fetchMeshState } from "../api";
import { recordResumeEvent } from "../utils/resumeInstrumentation";
const POLL_INTERVAL_MS = 10000;
const VISIBILITY_REFRESH_DEBOUNCE_MS = 1000;
export interface UseMeshStateResult {
meshState: NodeMeshState[];
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
}
export function useMeshState(): UseMeshStateResult {
const { t } = useTranslation("app");
const [meshState, setMeshState] = useState<NodeMeshState[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const lastVisibilityRefreshRef = useRef<number>(0);
const refresh = useCallback(async () => {
try {
setError(null);
const data = await fetchMeshState();
setMeshState(data.nodes);
} catch (err) {
setError(err instanceof Error ? err.message : t("mesh.failedToFetchMeshState", "Failed to fetch mesh state"));
}
}, [t]);
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
try {
const data = await fetchMeshState();
if (!cancelled) {
setMeshState(data.nodes);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : t("mesh.failedToFetchMeshState", "Failed to fetch mesh state"));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
void load();
const handleVisibilityChange = () => {
if (document.visibilityState !== "visible") return;
const now = Date.now();
const timeSinceLastRefresh = now - lastVisibilityRefreshRef.current;
if (timeSinceLastRefresh < VISIBILITY_REFRESH_DEBOUNCE_MS) {
recordResumeEvent({
view: "useMeshState",
trigger: "visibility",
projectId: undefined,
replayAttempted: false,
reason: "debounce-skipped",
detail: { timeSinceLastRefreshMs: timeSinceLastRefresh },
});
return;
}
lastVisibilityRefreshRef.current = now;
recordResumeEvent({
view: "useMeshState",
trigger: "visibility",
projectId: undefined,
replayAttempted: false,
reason: "debounced-refresh",
});
void refresh();
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
cancelled = true;
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [refresh]);
useEffect(() => {
if (loading) return;
intervalRef.current = setInterval(() => {
void refresh();
}, POLL_INTERVAL_MS);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [loading, refresh]);
return { meshState, loading, error, refresh };
}