diff --git a/gui/web/src/App.tsx b/gui/web/src/App.tsx index b5f4aff..0c2cda9 100644 --- a/gui/web/src/App.tsx +++ b/gui/web/src/App.tsx @@ -22,8 +22,18 @@ const TABS: { id: TabId; label: string }[] = [ ]; export default function App() { - const { machines, machine, devices, device, tab, loadingMachines, init, setMachine, setDevice, setTab } = - useStore(); + // Per-field selectors: subscribing to the whole store re-rendered the entire + // shell on every cursor scrub (setCursorMs); App doesn't read cursorMs. + const machines = useStore((s) => s.machines); + const machine = useStore((s) => s.machine); + const devices = useStore((s) => s.devices); + const device = useStore((s) => s.device); + const tab = useStore((s) => s.tab); + const loadingMachines = useStore((s) => s.loadingMachines); + const init = useStore((s) => s.init); + const setMachine = useStore((s) => s.setMachine); + const setDevice = useStore((s) => s.setDevice); + const setTab = useStore((s) => s.setTab); const removeMachine = useStore((s) => s.removeMachine); const clearMachines = useStore((s) => s.clearMachines); @@ -42,6 +52,17 @@ export default function App() { } }, [device, machines, devices]); // eslint-disable-line react-hooks/exhaustive-deps + // Honest data-source badge: a live backend with zero fetched shots still serves + // the mock machines, so key off the SELECTED machine's `mock` flag (from the + // backend), falling back to usingLiveBackend only when the flag is absent. + const selected = machines.find((m) => m.id === machine); + const mock = selected?.mock ?? !usingLiveBackend(); + const badgeText = !mock + ? "● live backend" + : usingLiveBackend() + ? "○ demo data (no shots fetched)" + : "○ offline / demo"; + // Deletable = real fetched shots (mock demo machines have nothing on disk), scoped // to the visible device. Only offer delete controls against a live backend. const deletable = usingLiveBackend() ? visibleMachines.filter((m) => !m.mock) : []; @@ -71,7 +92,7 @@ export default function App() { Magnetics 3D magnetic-sensor analysis - {usingLiveBackend() ? "● live backend" : "○ offline / demo"} + {badgeText} @@ -94,7 +115,7 @@ export default function App() { )}
-

Shot / machine

+

Shot / machine

{deletable.length > 0 && (
{loadingMachines &&
loading…
} - {visibleMachines.map((m) => ( -
setMachine(m.id)} - > -
-
{m.label}
- {m.note &&
{m.note}
} +
+ {visibleMachines.map((m) => ( + // role=option row (not a + )}
- {!m.mock && usingLiveBackend() && ( - - )} -
- ))} + ))} +
-
+
{TABS.map((t) => ( -
setTab(t.id)}> +
+ ))}
{!machine ? ( diff --git a/gui/web/src/components/ErrorBoundary.test.tsx b/gui/web/src/components/ErrorBoundary.test.tsx index 42529e0..c893c34 100644 --- a/gui/web/src/components/ErrorBoundary.test.tsx +++ b/gui/web/src/components/ErrorBoundary.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, expect, test, vi } from "vitest"; import ErrorBoundary from "./ErrorBoundary"; @@ -28,3 +28,23 @@ test("renders children unchanged when nothing throws", () => { ); expect(screen.getByText("healthy panel")).toBeInTheDocument(); }); + +test("Retry clears the error and recovers in place when the child stops throwing", () => { + let shouldThrow = true; + function Flaky(): React.ReactElement { + if (shouldThrow) throw new Error("transient payload"); + return
recovered panel
; + } + render( + + + , + ); + expect(screen.getByText(/failed to render/i)).toBeInTheDocument(); + + // The underlying condition is fixed; Retry should re-render children in place. + shouldThrow = false; + fireEvent.click(screen.getByRole("button", { name: /retry/i })); + expect(screen.getByText("recovered panel")).toBeInTheDocument(); + expect(screen.queryByText(/failed to render/i)).not.toBeInTheDocument(); +}); diff --git a/gui/web/src/components/ErrorBoundary.tsx b/gui/web/src/components/ErrorBoundary.tsx index 7b26287..00d03b0 100644 --- a/gui/web/src/components/ErrorBoundary.tsx +++ b/gui/web/src/components/ErrorBoundary.tsx @@ -48,6 +48,19 @@ export default class ErrorBoundary extends Component { Try a different shot or reload; other tabs are unaffected. +
+
); } diff --git a/gui/web/src/components/PullControl.tsx b/gui/web/src/components/PullControl.tsx index 31664e4..c1124f6 100644 --- a/gui/web/src/components/PullControl.tsx +++ b/gui/web/src/components/PullControl.tsx @@ -83,6 +83,10 @@ export default function PullControl() { if (!usingLiveBackend()) return null; const needsCreds = backend === "remote" || backend === "mdsthin"; const num = (s: string) => (s.trim() === "" ? undefined : Number(s)); + // Gate the Pull button: clearing the field or typing non-digits would otherwise + // POST shot 0/NaN to /api/fetch. + const shotNum = Number(shot); + const shotValid = Number.isFinite(shotNum) && shotNum > 0; async function pull() { setBusy(true); @@ -153,7 +157,8 @@ export default function PullControl() { ))} )} - setShot(e.target.value)} placeholder="shot number" /> setCursorMs(parseFloat(e.target.value))} @@ -1536,6 +1558,7 @@ export default function RotatingTab({ machine }: { machine: string }) {
) : error ? (
geometry unavailable: {error}
+ ) : !loading && !meta ? ( +
No sensor layout for this shot’s geometry.
) : null} {meta && ( @@ -401,10 +406,15 @@ export default function SensorsTab({ machine }: { machine: string }) { })}
Overlays
-