Files
fusion/packages/dashboard/app/components/MeshTopology.tsx
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

109 lines
5.2 KiB
TypeScript

import { memo, useMemo, type ReactElement } from "react";
import { useTranslation } from "react-i18next";
import type { NodeMeshState } from "@fusion/core";
export interface MeshTopologyProps {
nodes: NodeMeshState[];
className?: string;
}
const STATUS_COLORS: Record<NodeMeshState["status"], string> = {
online: "var(--success, var(--color-success))",
offline: "var(--text-dim)",
connecting: "var(--color-warning)",
error: "var(--color-error)",
};
const NODE_RADIUS = 28;
const LABEL_OFFSET = 12;
const MIN_VIEWBOX_SIZE = 300;
const MAX_REMOTE_DISTANCE = 120;
function MeshTopologyInner({ nodes, className }: MeshTopologyProps): ReactElement {
const { t } = useTranslation("app");
const localNode = useMemo(() => nodes.find((n) => n.nodeType === "local") ?? nodes[0], [nodes]);
const remoteNodes = useMemo(() => nodes.filter((n) => n.nodeId !== localNode?.nodeId), [nodes, localNode?.nodeId]);
const viewBoxSize = useMemo(() => MIN_VIEWBOX_SIZE + (Math.max(0, remoteNodes.length - 4) * 20), [remoteNodes.length]);
const centerX = viewBoxSize / 2;
const centerY = viewBoxSize / 2;
const nodePositions = useMemo(() => {
const positions = new Map<string, { x: number; y: number; node: NodeMeshState }>();
if (localNode) {
positions.set(localNode.nodeId, { x: centerX, y: centerY, node: localNode });
}
if (remoteNodes.length > 0) {
const distance = Math.min(MAX_REMOTE_DISTANCE, (viewBoxSize / 2) - NODE_RADIUS - 10);
const angleStep = (2 * Math.PI) / remoteNodes.length;
const startAngle = -Math.PI / 2;
remoteNodes.forEach((node, index) => {
const angle = startAngle + index * angleStep;
positions.set(node.nodeId, {
node,
x: centerX + distance * Math.cos(angle),
y: centerY + distance * Math.sin(angle),
});
});
}
return positions;
}, [centerX, centerY, localNode, remoteNodes, viewBoxSize]);
const links = useMemo(() => {
const seen = new Set<string>();
const result: Array<{ key: string; from: { x: number; y: number }; to: { x: number; y: number } }> = [];
for (const node of nodes) {
const from = nodePositions.get(node.nodeId);
if (!from) continue;
for (const peer of node.knownPeers) {
const to = nodePositions.get(peer.peerNodeId);
if (!to) continue;
const key = [node.nodeId, peer.peerNodeId].sort().join("::");
if (seen.has(key)) continue;
seen.add(key);
result.push({ key, from, to });
}
}
return result;
}, [nodePositions, nodes]);
if (nodes.length === 0) {
return <div className={`mesh-topology mesh-topology--empty ${className ?? ""}`}><div className="mesh-topology__empty-state"><p>{t("mesh.noNodes", "No nodes to display")}</p></div></div>;
}
return (
<div className={`mesh-topology ${className ?? ""}`}>
<svg className="mesh-topology__svg" viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`} preserveAspectRatio="xMidYMid meet" aria-label={t("mesh.ariaLabel", "Node mesh topology visualization")}>
{links.map((link) => (
<line key={link.key} className="mesh-topology__link mesh-topology__peer-line" x1={link.from.x} y1={link.from.y} x2={link.to.x} y2={link.to.y} />
))}
{Array.from(nodePositions.values()).map(({ node, x, y }) => (
<g key={node.nodeId} className="mesh-topology__node" transform={`translate(${x}, ${y})`}>
<circle className="mesh-topology__node-circle" r={NODE_RADIUS} fill={STATUS_COLORS[node.status]} aria-label={`${node.nodeName} (${node.status})`} />
<text className="mesh-topology__node-label" y={NODE_RADIUS + LABEL_OFFSET} textAnchor="middle">
{node.nodeName.length > 12 ? `${node.nodeName.slice(0, 10)}…` : node.nodeName}
</text>
<g className="mesh-topology__node-type" transform={`translate(0 ${-NODE_RADIUS - 10})`}>
<circle className="mesh-topology__node-type-badge" r="8" />
<text className="mesh-topology__node-type-text" textAnchor="middle" dominantBaseline="middle">
{node.nodeType === "local" ? "L" : "R"}
</text>
</g>
</g>
))}
</svg>
<div className="mesh-topology__legend">
<div className="mesh-topology__legend-item"><span className="mesh-topology__legend-dot" style={{ background: STATUS_COLORS.online }} /><span>{t("mesh.online", "Online")}</span></div>
<div className="mesh-topology__legend-item"><span className="mesh-topology__legend-dot" style={{ background: STATUS_COLORS.offline }} /><span>{t("mesh.offline", "Offline")}</span></div>
<div className="mesh-topology__legend-item"><span className="mesh-topology__legend-dot" style={{ background: STATUS_COLORS.connecting }} /><span>{t("mesh.connecting", "Connecting")}</span></div>
<div className="mesh-topology__legend-item"><span className="mesh-topology__legend-dot" style={{ background: STATUS_COLORS.error }} /><span>{t("mesh.error", "Error")}</span></div>
</div>
{links.length === 0 && <p className="mesh-topology__notice">{t("mesh.noPeers", "Peer-to-peer discovery data unavailable.")}</p>}
</div>
);
}
export const MeshTopology = memo(MeshTopologyInner);