Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d086935
refactor(simulator): extract Sidebar UI into SimulatorSidebar compone…
ttbombadil Feb 19, 2026
15aa8bf
refactor(simulator): accept visual baseline + provider refactor
ttbombadil Feb 19, 2026
9d0f4ef
fix(ts): resolve tsc errors — remove unused vars and guard optional c…
ttbombadil Feb 19, 2026
895640d
fix(e2e): accept sim_telemetry regardless of local status to avoid race
ttbombadil Feb 19, 2026
9c58d8e
chore(triage): log incoming sim_telemetry in useWebSocketHandler for e2e
ttbombadil Feb 19, 2026
4e5f903
fix(sim): provide simulation & serial state to output/sidebar — resto…
ttbombadil Feb 19, 2026
31e6eb3
fix(styles): replace raw-hex with design token (bg-status-success) in…
ttbombadil Feb 19, 2026
7d111f4
chore: cleanup triage logs and standardize logging
ttbombadil Feb 19, 2026
38ea8f4
fix(code-editor): avoid raw-hex fallback for Monaco theme (use named …
ttbombadil Feb 19, 2026
0cdc767
test(e2e): robust simulate toggle clicks; memoize/stabilize Simulator…
ttbombadil Feb 19, 2026
b9c0df3
test(visual): update simulator visual baseline
ttbombadil Feb 19, 2026
0dd19ba
test(e2e): retry on 429 in startSimulation; style SimulatorHeader
ttbombadil Feb 19, 2026
8cfd525
fix(ws): suppress serial_output emitted after runner.stop() to avoid …
ttbombadil Feb 19, 2026
3fcc5ea
chore(ws): add debug logs around serial forwarding & stop handling (d…
ttbombadil Feb 19, 2026
f6ab52f
fix(ws): add per-run runId to suppress post-stop callbacks; clear run…
ttbombadil Feb 19, 2026
09b7db5
test(e2e): wait for stop marker before asserting no further output
ttbombadil Feb 19, 2026
f657947
refactor(simulator): simulation ui provider & stability fixes
ttbombadil Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"/^bash /Users/to/sciebo/TT_Web/UNOWEBSIM_github_dupe/scripts/frontend-source-analysis\\.sh$/": {
"approve": true,
"matchCommandLine": true
}
},
"git rev-parse": true
},
}
128 changes: 4 additions & 124 deletions client/src/components/features/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,28 +113,18 @@ export const AppHeader: React.FC<AppHeaderProps> = ({
const headerRef = React.useRef<HTMLElement | null>(null);
const leftGroupRef = React.useRef<HTMLDivElement | null>(null);
const centerGroupRef = React.useRef<HTMLDivElement | null>(null);
const [centerLeft, setCenterLeft] = React.useState<number | null>(null);

React.useLayoutEffect(() => {
if (isMobile) return;
const headerEl = headerRef.current;
const leftEl = leftGroupRef.current;
const centerEl = centerGroupRef.current;
if (!headerEl || !leftEl || !centerEl) return;

// keep layout observer for responsive header positioning but no local state
const computeCenter = () => {
const headerRect = headerEl.getBoundingClientRect();
const leftRect = leftEl.getBoundingClientRect();
const centerRect = centerEl.getBoundingClientRect();
const gap = 12;
const leftEdge = leftRect.left - headerRect.left;
const minCenter =
leftEdge + leftRect.width + gap + centerRect.width / 2;
const target = Math.max(headerRect.width / 2, minCenter);
setCenterLeft(target);
/* intentionally no-op; observers remain for future layout-driven changes */
};

computeCenter();
const observer = new ResizeObserver(() => computeCenter());
observer.observe(headerEl);
observer.observe(leftEl);
Expand Down Expand Up @@ -383,118 +373,8 @@ export const AppHeader: React.FC<AppHeaderProps> = ({

<div className="flex-1" />

{/* Center: Simulate Button */}
<div
ref={centerGroupRef}
className="absolute top-1/2"
style={{
left: centerLeft ? `${centerLeft}px` : "50%",
transform: "translate(-50%, -50%)",
}}
>
<Button
onClick={
simulationStatus === "running"
? onStop
: simulationStatus === "paused"
? onResume
: onSimulate
}
disabled={simulateDisabled}
className={clsx(
"h-[var(--ui-button-height)] px-4 pr-12 min-w-[10rem] flex items-center justify-center gap-2 relative",
"!text-white font-medium transition-colors",
{
"!bg-status-warning hover:!bg-accent-amber":
simulationStatus === "running" && !simulateDisabled,
"!bg-status-success hover:!bg-status-success-dark":
(simulationStatus === "stopped" || simulationStatus === "paused") &&
!simulateDisabled,
"opacity-50 cursor-not-allowed bg-gray-500 hover:!bg-gray-500":
simulateDisabled,
},
)}
data-testid="button-simulate-toggle"
aria-label={
simulationStatus === "running"
? "Stop Simulation"
: simulationStatus === "paused"
? "Resume Simulation"
: "Start Simulation"
}
>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center gap-2 pointer-events-none">
<div className="relative w-4 h-4">
<Play
className={clsx(
"absolute inset-0 m-auto h-4 w-4 transition-all duration-200",
{
"opacity-100 scale-100": !isLoading && simulationStatus !== "running",
"opacity-0 scale-75": isLoading || simulationStatus === "running",
},
)}
/>
<Square
className={clsx(
"absolute inset-0 m-auto h-4 w-4 transition-all duration-200",
{
"opacity-100 scale-100": !isLoading && simulationStatus === "running",
"opacity-0 scale-75": isLoading || simulationStatus !== "running",
},
)}
/>
<Loader2
className={clsx(
"absolute inset-0 m-auto h-4 w-4 transition-opacity duration-150",
{ "opacity-100": isLoading, "opacity-0": !isLoading },
)}
/>
</div>
<span className="font-semibold leading-none">
{simulationStatus === "running"
? "Stop"
: simulationStatus === "paused"
? "Resume"
: "Start"}
</span>
</div>
{simulationStatus === "running" && (
<div
className="absolute right-0 top-0 bottom-0 pl-2 border-l border-orange-500/50 flex items-center cursor-pointer bg-yellow-400/90 hover:bg-yellow-400 pr-2 rounded-r z-10"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!simulateDisabled && !isLoading) {
onPause();
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
if (!simulateDisabled && !isLoading) {
onPause();
}
}
}}
aria-label="Pause Simulation"
title="Pause"
>
{isPausing ? (
<Loader2 className="h-3 w-3 animate-spin text-orange-900" />
) : (
<Pause className="h-3 w-3 text-orange-900" />
)}
</div>
)}
</Button>
</div>
{/* Center simulate control moved to `SimulatorHeader` (keeps AppHeader small) */}
<div className="flex-1" />

{/* Right: Optional telemetry/extra controls */}
<div className="flex-1 flex items-center justify-end min-w-0">
Expand Down
12 changes: 6 additions & 6 deletions client/src/components/features/arduino-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function ArduinoBoard({
setSvgContent(main);
setOverlaySvgContent(overlay);
})
.catch((err) => console.error("Failed to load Arduino SVGs:", err));
.catch((err) => logger.error(`Failed to load Arduino SVGs: ${String(err)}`));
}, []);

// Listen for color changes from settings dialog (custom event)
Expand Down Expand Up @@ -192,7 +192,7 @@ export function ArduinoBoard({

// Single stable polling loop for ALL SVG updates - runs ONCE, never restarts
useEffect(() => {
console.log("[ArduinoBoard] Starting stable polling loop");
logger.debug("[ArduinoBoard] Starting stable polling loop");
const performAllUpdates = () => {
// Check overlay ref INSIDE the callback to handle late mounting
const overlay = overlayRef.current;
Expand Down Expand Up @@ -850,8 +850,8 @@ export function ArduinoBoard({
{debugMode && telemetry && isSimulationRunning && (
<div className="ml-4 flex items-center gap-4 text-xs text-muted-foreground border-l border-muted-foreground/30 pl-4" data-testid="telemetry-metrics">
<div className="flex flex-col" data-testid="telemetry-pin-changes">
<span className="text-[10px] uppercase tracking-wider text-cyan-500/50">Pin Changes</span>
<span className="text-sm font-mono text-cyan-400" data-testid="telemetry-pin-changes-value">
<span className="text-ui-xs uppercase tracking-wider text-cyan-500/50">Pin Changes</span>
<span className="text-ui-xs font-mono text-cyan-400" data-testid="telemetry-pin-changes-value">
{telemetry.intendedPinChangesPerSecond.toFixed(0)} /s
{telemetry.droppedPinChangesPerSecond > 0 && (
<span className="ml-1 text-amber-400/80" data-testid="telemetry-dropped">
Expand All @@ -861,8 +861,8 @@ export function ArduinoBoard({
</span>
</div>
<div className="flex flex-col" data-testid="telemetry-batching">
<span className="text-[10px] uppercase tracking-wider text-cyan-500/50">Batching</span>
<span className="text-sm font-mono text-cyan-400" data-testid="telemetry-batching-value">
<span className="text-ui-xs uppercase tracking-wider text-cyan-500/50">Batching</span>
<span className="text-ui-sm font-mono text-cyan-400" data-testid="telemetry-batching-value">
{telemetry.batchesPerSecond.toFixed(0)} bat/s · {telemetry.avgStatesPerBatch.toFixed(0)} st/bat
</span>
</div>
Expand Down
60 changes: 49 additions & 11 deletions client/src/components/features/code-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,44 @@ export function CodeEditor({
});

// Configure theme
// Define theme using semantic CSS tokens where possible (fallback to the
// original hex values if CSS vars are not available). This prevents raw-hex
// literals from appearing in client source while keeping Monaco theming
// deterministic and responsive to design tokens.
const cssVar = (name: string, fallback?: string) => {
try {
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return v || fallback || "";
} catch {
return fallback || "";
}
};

// Normalize CSS color values (hsl, named colors, hex) into rgb()/rgba() strings
// Monaco's theme parser rejects some formats (e.g. hsl()), so use canvas to
// obtain canonical rgba values at runtime.
const toRgbaString = (colorStr: string): string => {
try {
const c = document.createElement("canvas");
c.width = c.height = 1;
const ctx = c.getContext("2d");
if (!ctx) return colorStr || "rgba(0,0,0,1)";
ctx.clearRect(0, 0, 1, 1);
ctx.fillStyle = colorStr || "black";
ctx.fillRect(0, 0, 1, 1);
const d = ctx.getImageData(0, 0, 1, 1).data;
const r = d[0], g = d[1], b = d[2], a = +(d[3] / 255).toFixed(3);
if (a >= 1) {
// produce hex string at runtime (accepted by Monaco)
const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
return `#${hex}`;
}
return `rgba(${r}, ${g}, ${b}, ${a})`;
} catch {
return colorStr || "black";
}
};

monaco.editor.defineTheme("arduino-dark", {
base: "vs-dark",
inherit: true,
Expand All @@ -130,12 +168,12 @@ export function CodeEditor({
{ token: "operator", foreground: "d4d4d4" },
],
colors: {
"editor.background": "#121212",
"editor.foreground": "#fafafa",
"editorLineNumber.foreground": "#666666",
"editorLineNumber.activeForeground": "#ffffff",
"editor.selectionBackground": "#262626",
"editor.lineHighlightBackground": "#121212",
"editor.background": toRgbaString(cssVar("--background") || "black"),
"editor.foreground": toRgbaString(cssVar("--foreground") || "white"),
"editorLineNumber.foreground": toRgbaString(cssVar("--muted-foreground") || "gray"),
"editorLineNumber.activeForeground": toRgbaString(cssVar("--foreground") || "white"),
"editor.selectionBackground": toRgbaString((cssVar("--card") || "rgb(34,34,34)").trim()),
"editor.lineHighlightBackground": toRgbaString(cssVar("--background") || "black"),
},
});

Expand All @@ -151,7 +189,7 @@ export function CodeEditor({
const lh = baseLh * scale;
return { fs, lh };
} catch (e) {
console.warn("computeUi failed", e);
logger.warn(`computeUi failed: ${String(e)}`);
return { fs: 16, lh: 20 };
}
};
Expand Down Expand Up @@ -210,7 +248,7 @@ export function CodeEditor({
editor.layout();
} catch {}
} catch (err) {
console.warn("Monaco initial rAF sync failed", err);
logger.warn(`Monaco initial rAF sync failed: ${String(err)}`);
}
});

Expand Down Expand Up @@ -302,7 +340,7 @@ export function CodeEditor({
});
}
} catch (err) {
console.error("Insert text at line failed:", err);
logger.error(`Insert text at line failed: ${String(err)}`);
}
},
insertSuggestionSmartly: (
Expand Down Expand Up @@ -395,7 +433,7 @@ export function CodeEditor({
column: 1,
});
} catch (err) {
console.error("Insert suggestion smartly failed:", err);
logger.error(`Insert suggestion smartly failed: ${String(err)}`);
}
},
copy: () => {
Expand Down Expand Up @@ -535,7 +573,7 @@ export function CodeEditor({
} catch {}
}
} catch (err) {
console.warn("onScale failed", err);
logger.warn(`onScale failed: ${String(err)}`);
}
};

Expand Down
6 changes: 4 additions & 2 deletions client/src/components/features/examples-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { Button } from "@/components/ui/button";
import { BookOpen, ChevronRight } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Logger } from "@shared/logger";
const logger = new Logger("ExamplesMenu");

interface Example {
name: string;
Expand Down Expand Up @@ -63,15 +65,15 @@ export function ExamplesMenu({
});
}
} catch (error) {
console.error(`Failed to load example ${filename}:`, error);
logger.error(`Failed to load example ${filename}: ${String(error)}`);
}
}

// Sort examples by filename
loadedExamples.sort((a, b) => a.filename.localeCompare(b.filename));
setExamples(loadedExamples);
} catch (error) {
console.error("Failed to load examples:", error);
logger.error(`Failed to load examples: ${String(error)}`);
toast({
title: "Failed to Load Examples",
description: "Could not load example files",
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/features/pin-monitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ export function PinMonitor({ pinStates, batchStats }: PinMonitorProps) {
data-testid="pin-monitor"
>
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-semibold text-foreground">Pin Monitor</div>
<div className="text-ui-sm font-semibold text-foreground">Pin Monitor</div>
<button
type="button"
className="text-xs text-muted-foreground hover:text-foreground"
className="text-ui-xs text-muted-foreground hover:text-foreground"
onClick={() => setShowPerf((prev) => !prev)}
>
{showPerf ? "Hide FPS" : "Show FPS"}
Expand Down Expand Up @@ -68,7 +68,7 @@ export function PinMonitor({ pinStates, batchStats }: PinMonitorProps) {
key={state.pin}
data-pin={state.pin}
className={clsx(
"flex items-center justify-between rounded-md border px-2 py-1 text-xs",
"flex items-center justify-between rounded-md border px-2 py-1 text-ui-xs",
isHigh && !isPwm
? "border-green-400 text-green-500"
: "border-border text-muted-foreground",
Expand Down
1 change: 1 addition & 0 deletions client/src/components/features/serial-monitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export function SerialMonitor({
viewportRef={outputRef}
viewportTestId="serial-output"
viewportProps={{ onScroll: handleScroll }}
// use token so font-size follows --ui-font-scale
viewportClassName="p-3 text-ui-xs font-mono"
thumbClassName="bg-status-success"
/>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/features/settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export default function SettingsDialog({
Enable debug UI elements (telemetry displays, status light, CLI/GCC labels).
</div>
<div className="text-ui-xs text-muted-foreground mt-1">
<kbd className="px-1.5 py-0.5 bg-background rounded border text-[10px]">
<kbd className="px-1.5 py-0.5 bg-background rounded border text-ui-xs">
{navigator.platform.toLowerCase().includes('mac') ? '⌘' : 'Strg'}+D
</kbd>
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/features/sim-cockpit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const SimCockpit: React.FC<{
const isActive = isSimActive && lastHeartbeatAt && Date.now() - lastHeartbeatAt < 2000;

return (
<div className="hidden lg:flex items-center gap-6 bg-black/20 backdrop-blur-md border border-white/10 rounded-lg px-4 py-2 text-[10px] uppercase tracking-wider font-medium shadow-2xl">
<div className="hidden lg:flex items-center gap-6 bg-black/20 backdrop-blur-md border border-white/10 rounded-lg px-4 py-2 text-ui-xs uppercase tracking-wider font-medium shadow-2xl">
{/* Health Indicator - Link State Only */}
<div className="flex items-center gap-3">
<div className="flex flex-col items-end">
<span className="text-white/40 leading-none mb-1 text-right">Link State</span>
<span className={clsx("text-[9px] font-bold", isActive ? "text-emerald-400" : "text-red-500")}>
<span className={clsx("text-ui-xs font-bold", isActive ? "text-emerald-400" : "text-red-500")}>
{isActive ? "STABLE" : "DISCONNECTED"}
</span>
</div>
Expand Down
Loading
Loading