diff --git a/.vscode/settings.json b/.vscode/settings.json index fddd8f92..8fab1e07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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 }, } diff --git a/client/src/components/features/app-header.tsx b/client/src/components/features/app-header.tsx index 6740488e..e53547aa 100644 --- a/client/src/components/features/app-header.tsx +++ b/client/src/components/features/app-header.tsx @@ -113,8 +113,6 @@ export const AppHeader: React.FC = ({ const headerRef = React.useRef(null); const leftGroupRef = React.useRef(null); const centerGroupRef = React.useRef(null); - const [centerLeft, setCenterLeft] = React.useState(null); - React.useLayoutEffect(() => { if (isMobile) return; const headerEl = headerRef.current; @@ -122,19 +120,11 @@ export const AppHeader: React.FC = ({ 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); @@ -383,118 +373,8 @@ export const AppHeader: React.FC = ({
- {/* Center: Simulate Button */} -
- -
+ {/* Center simulate control moved to `SimulatorHeader` (keeps AppHeader small) */} +
{/* Right: Optional telemetry/extra controls */}
diff --git a/client/src/components/features/arduino-board.tsx b/client/src/components/features/arduino-board.tsx index c43f07fd..e79b6801 100644 --- a/client/src/components/features/arduino-board.tsx +++ b/client/src/components/features/arduino-board.tsx @@ -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) @@ -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; @@ -850,8 +850,8 @@ export function ArduinoBoard({ {debugMode && telemetry && isSimulationRunning && (
- Pin Changes - + Pin Changes + {telemetry.intendedPinChangesPerSecond.toFixed(0)} /s {telemetry.droppedPinChangesPerSecond > 0 && ( @@ -861,8 +861,8 @@ export function ArduinoBoard({
- Batching - + Batching + {telemetry.batchesPerSecond.toFixed(0)} bat/s · {telemetry.avgStatesPerBatch.toFixed(0)} st/bat
diff --git a/client/src/components/features/code-editor.tsx b/client/src/components/features/code-editor.tsx index 5842640e..19ad10b5 100644 --- a/client/src/components/features/code-editor.tsx +++ b/client/src/components/features/code-editor.tsx @@ -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, @@ -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"), }, }); @@ -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 }; } }; @@ -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)}`); } }); @@ -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: ( @@ -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: () => { @@ -535,7 +573,7 @@ export function CodeEditor({ } catch {} } } catch (err) { - console.warn("onScale failed", err); + logger.warn(`onScale failed: ${String(err)}`); } }; diff --git a/client/src/components/features/examples-menu.tsx b/client/src/components/features/examples-menu.tsx index d33fb226..9c158233 100644 --- a/client/src/components/features/examples-menu.tsx +++ b/client/src/components/features/examples-menu.tsx @@ -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; @@ -63,7 +65,7 @@ export function ExamplesMenu({ }); } } catch (error) { - console.error(`Failed to load example ${filename}:`, error); + logger.error(`Failed to load example ${filename}: ${String(error)}`); } } @@ -71,7 +73,7 @@ export function ExamplesMenu({ 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", diff --git a/client/src/components/features/pin-monitor.tsx b/client/src/components/features/pin-monitor.tsx index e901aed5..b4350595 100644 --- a/client/src/components/features/pin-monitor.tsx +++ b/client/src/components/features/pin-monitor.tsx @@ -36,10 +36,10 @@ export function PinMonitor({ pinStates, batchStats }: PinMonitorProps) { data-testid="pin-monitor" >
-
Pin Monitor
+
Pin Monitor
- + {navigator.platform.toLowerCase().includes('mac') ? '⌘' : 'Strg'}+D
diff --git a/client/src/components/features/sim-cockpit.tsx b/client/src/components/features/sim-cockpit.tsx index 135bde29..341b412b 100644 --- a/client/src/components/features/sim-cockpit.tsx +++ b/client/src/components/features/sim-cockpit.tsx @@ -15,12 +15,12 @@ export const SimCockpit: React.FC<{ const isActive = isSimActive && lastHeartbeatAt && Date.now() - lastHeartbeatAt < 2000; return ( -
+
{/* Health Indicator - Link State Only */}
Link State - + {isActive ? "STABLE" : "DISCONNECTED"}
diff --git a/client/src/components/features/simulator/SimulatorHeader.tsx b/client/src/components/features/simulator/SimulatorHeader.tsx new file mode 100644 index 00000000..ee3b5418 --- /dev/null +++ b/client/src/components/features/simulator/SimulatorHeader.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import { Play, Zap, Server } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + +type Props = { + simulationStatus: "idle" | "running" | "compiling" | "stopped" | "paused"; + simulateDisabled?: boolean; + isCompiling?: boolean; + isStarting?: boolean; + isStopping?: boolean; + isPausing?: boolean; + isResuming?: boolean; + onSimulate?: () => void; + onStop?: () => void; + onPause?: () => void; + onResume?: () => void; + simulationTimeout?: number; + onTimeoutChange?: (n: number) => void; + onCompile: () => void; + onCompileAndStart: () => void; + board?: string; +}; + +function getStatusTextClass(status: Props["simulationStatus"]) { + switch (status) { + case "idle": + return "text-gray-500 italic"; + case "running": + return "text-green-600"; + case "stopped": + return "text-gray-600"; + default: + return ""; + } +} + +function SimulatorHeader({ + simulationStatus, + simulateDisabled = false, + isCompiling = false, + isStarting = false, + isPausing = false, + isResuming = false, + onSimulate, + onStop, + onPause, + onResume, + simulationTimeout, + onCompile, + onCompileAndStart, + board = "Arduino UNO", +}: Props) { + // keep a stable ref to the latest status so the click handler can be memoized + const statusRef = React.useRef(simulationStatus); + React.useEffect(() => { + statusRef.current = simulationStatus; + }, [simulationStatus]); + + const handleSimulateClick = React.useCallback(() => { + const status = statusRef.current; + if (status === "running") { + onStop?.(); + return; + } + if (status === "paused") { + onResume?.(); + return; + } + onSimulate?.(); + }, [onSimulate, onStop, onResume]); + + return ( +
+ {/* Simulate toggle (compact desktop) */} +
+
+ + + {/* Pause / Resume small control to the right of Start */} + {(simulationStatus === "running" || simulationStatus === "paused") && ( + + )} +
+ + {/* timeout display + small setter */} +
+ {simulationTimeout ?? 60}s +
+
+ + {/* Simulate toggle (mobile) */} +
+ + + {(simulationStatus === "running" || simulationStatus === "paused") && ( + + )} +
+ + {/* Compile button + small action menu */} + + + + + + Compile + Compile & Run + + + + {/* Board selector (display-only for now) */} +
+ + {board} +
+ + {/* Status text (keeps visual parity with previous header) */} +
+ {simulationStatus} +
+
+ ); +} + +// Memoize to reduce unnecessary re-renders and improve DOM stability during E2E interactions +export const MemoizedSimulatorHeader = React.memo(SimulatorHeader); +export default MemoizedSimulatorHeader; diff --git a/client/src/components/features/simulator/SimulatorOutputPanel.tsx b/client/src/components/features/simulator/SimulatorOutputPanel.tsx new file mode 100644 index 00000000..e9df001b --- /dev/null +++ b/client/src/components/features/simulator/SimulatorOutputPanel.tsx @@ -0,0 +1,312 @@ +import React, { Suspense } from "react"; +import { InputGroup } from "@/components/ui/input-group"; +import { SerialMonitor } from "@/components/features/serial-monitor"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; +import { Button } from "@/components/ui/button"; +import { Terminal, BarChart, Columns, Trash2, ChevronsDown, LayoutGrid, Table } from "lucide-react"; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; +import { useWebSocket } from "@/hooks/use-websocket"; +import { useToast } from "@/hooks/use-toast"; +import { useQueryClient } from "@tanstack/react-query"; +import { useBackendHealth } from "@/hooks/use-backend-health"; +import { useTelemetryStore } from "@/hooks/use-telemetry-store"; + +const SerialPlotter = React.lazy(() => import("@/components/features/serial-plotter").then((m) => ({ default: m.SerialPlotter }))); + +const LoadingPlaceholder = () => ( +
+ Loading chart... +
+); + +export const DebugConsole: React.FC = () => { + const ui = useSimulationUi(); + const { toast } = useToast(); + const debugMessages = ui.debugMessages; + const debugViewMode = ui.debugViewMode; + const setDebugViewMode = ui.setDebugViewMode; + const debugMessageFilter = ui.debugMessageFilter; + const setDebugMessageFilter = ui.setDebugMessageFilter; + const debugMessagesContainerRef = ui.debugMessagesContainerRef; + const setDebugMessages = ui.setDebugMessages; + + if (!ui.debugMode) return null; + + return ( +
+
+
+ Filter: + +
+ +
+
+ ); +}; + + +export default function SimulatorOutputPanel(props: { + simulationStatus?: string; + serialOutput?: any[]; + renderedSerialOutput?: any[]; + serialViewMode?: "monitor" | "plotter" | "both"; + autoScrollEnabled?: boolean; + setAutoScrollEnabled?: (v: boolean) => void; + serialInputValue?: string; + setSerialInputValue?: (v: string) => void; + showSerialMonitor?: boolean; + showSerialPlotter?: boolean; + cycleSerialViewMode?: () => void; + clearSerialOutput?: () => void; +} = {}) { + const queryClient = useQueryClient(); + const { ensureBackendConnected } = useBackendHealth(queryClient); + const { toast } = useToast(); + const { sendMessage } = useWebSocket(); + + const telemetryData = useTelemetryStore(); + + // Prefer props (page-provided) but fall back to provider/context values for + // backwards compatibility. The provider may not be passed the page-level + // serial state after the refactor, so prefer explicit props from the page. + const ui = useSimulationUi(); + + const serialOutput = props.serialOutput ?? ui.serialOutput ?? []; + const renderedSerialOutput = props.renderedSerialOutput ?? ui.renderedSerialOutput ?? []; + const serialViewMode = props.serialViewMode ?? ui.serialViewMode ?? "monitor"; + const autoScrollEnabled = props.autoScrollEnabled ?? ui.autoScrollEnabled ?? false; + const setAutoScrollEnabled = props.setAutoScrollEnabled ?? ui.setAutoScrollEnabled; + const serialInputValue = props.serialInputValue ?? ui.serialInputValue ?? ""; + const setSerialInputValue = props.setSerialInputValue ?? ui.setSerialInputValue; + const showSerialMonitor = props.showSerialMonitor ?? ui.showSerialMonitor ?? true; + const showSerialPlotter = props.showSerialPlotter ?? ui.showSerialPlotter ?? false; + const cycleSerialViewMode = props.cycleSerialViewMode ?? ui.cycleSerialViewMode; + const clearSerialOutput = props.clearSerialOutput ?? ui.clearSerialOutput; + + const simulationStatus = props.simulationStatus ?? ui.simulationStatus; + const setTxActivity = ui.setTxActivity; + const debugMode = ui.debugMode; + + const handleSerialSend = (message: string) => { + if (!ensureBackendConnected("Serial senden")) return; + + if (simulationStatus !== "running") { + toast({ + title: simulationStatus === "paused" ? "Simulation paused" : "Simulation not running", + description: + simulationStatus === "paused" + ? "Resume the simulation to send serial input." + : "Start the simulation to send serial input.", + variant: "destructive", + }); + return; + } + + setTxActivity?.((prev) => prev + 1); + sendMessage({ type: "serial_input", data: message }); + }; + + const handleSerialInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") handleSerialSend(serialInputValue); + }; + + return ( +
+ {/* Static Serial Header (always full width) */} +
+
+
+ Serial Output +
+ + {debugMode && (simulationStatus === "running" || simulationStatus === "paused") && telemetryData.last ? ( +
+
+ Serial Events + {(telemetryData.last.serialOutputPerSecond ?? 0).toFixed(1)} /s +
+
+ Serial Bytes + {(telemetryData.last.serialBytesPerSecond ?? 0).toFixed(1)} /s +
+
+ ) : null} + +
+ +
+ + + + + +
+
+
+ +
+ {/* Serial area */} + {showSerialMonitor && showSerialPlotter ? ( + + +
+
+ {})} + showMonitor={showSerialMonitor} + autoScrollEnabled={autoScrollEnabled} + /> +
+
+
+ + + + +
+ }> + + +
+
+
+ ) : showSerialMonitor ? ( +
+
+ {})} + showMonitor={showSerialMonitor} + autoScrollEnabled={autoScrollEnabled} + /> +
+
+ ) : ( +
+ }> + + +
+ )} +
+ + {/* Input area */} +
+
+ setSerialInputValue?.(e.target.value)} + onKeyDown={handleSerialInputKeyDown} + onSubmit={() => handleSerialSend(serialInputValue)} + disabled={!serialInputValue.trim() || simulationStatus !== "running"} + inputTestId="input-serial" + buttonTestId="button-send-serial" + /> +
+
+ + {/* Debug console: extracted so it can be shown as a dedicated "Telemetry" tab */} + {/* DebugConsole component exported below for reuse in the Output tabs */} +
+ ); +} diff --git a/client/src/components/features/simulator/SimulatorSidebar.tsx b/client/src/components/features/simulator/SimulatorSidebar.tsx new file mode 100644 index 00000000..3e595280 --- /dev/null +++ b/client/src/components/features/simulator/SimulatorSidebar.tsx @@ -0,0 +1,225 @@ +import { PinMonitor } from "@/components/features/pin-monitor"; +import { ArduinoBoard } from "@/components/features/arduino-board"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { CompilationOutput } from "@/components/features/compilation-output"; +import { ParserOutput } from "@/components/features/parser-output"; +import { ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; +import { X } from "lucide-react"; +import { useSimulationStore } from "@/hooks/use-simulation-store"; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; +import { DebugConsole } from "./SimulatorOutputPanel"; + + + +export type OutputApi = { + cliOutput: string; + handleClearCompilationOutput: () => void; + isSuccessState: boolean; + isModified: boolean; + parserMessages: any[]; + ioRegistry: any[]; + parserMessagesContainerRef: React.RefObject; + activeOutputTab: "compiler" | "messages" | "registry" | "debug"; + setActiveOutputTab: (v: any) => void; + showCompilationOutput: boolean; + setShowCompilationOutput: (v: boolean) => void; + setParserPanelDismissed: (v: boolean) => void; + outputPanelRef: any; + outputTabsHeaderRef: React.RefObject; + outputPanelMinPercent: number; + compilationPanelSize: number; + setCompilationPanelSize: (n: number) => void; + outputPanelManuallyResizedRef: React.MutableRefObject; + openOutputPanel: (tab: any) => void; + toast: (args: any) => void; + setParserPanelDismissedLocal?: (v: boolean) => void; +}; + +export default function SimulatorSidebar({ + pinMonitorVisible = true, + onReset, + onPinToggle, + analogPins = [], + onAnalogChange, + isMobile = false, + // optional props — prefer page-provided values but fall back to context + simulationStatus: propSimulationStatus, + txActivity: propTxActivity, + rxActivity: propRxActivity, +}: Partial<{ + pinMonitorVisible: boolean; + onReset: () => void; + onPinToggle: (pin: number, newValue: number) => void; + analogPins: number[]; + onAnalogChange: (pin: number, newValue: number) => void; + isMobile?: boolean; + simulationStatus?: string; + txActivity?: number; + rxActivity?: number; +}>) { + const { pinStates, batchStats } = useSimulationStore(); + const ui = useSimulationUi(); + const simulationStatus = (propSimulationStatus as any) ?? ui.simulationStatus; + const txActivity = (propTxActivity as any) ?? ui.txActivity; + const rxActivity = (propRxActivity as any) ?? ui.rxActivity; + const isRunning = simulationStatus === "running"; + + return ( +
+ {pinMonitorVisible && ( +
+ +
+ )} + +
+ +
+
+ ); +} + +export function SimulatorOutput({ outputApi }: { outputApi: OutputApi }) { + const { + cliOutput, + handleClearCompilationOutput, + isSuccessState, + isModified, + parserMessages, + ioRegistry, + parserMessagesContainerRef, + activeOutputTab, + setActiveOutputTab, + setParserPanelDismissed, + outputPanelRef, + outputTabsHeaderRef, + outputPanelMinPercent, + compilationPanelSize, + setCompilationPanelSize, + outputPanelManuallyResizedRef, + openOutputPanel, + setShowCompilationOutput, + } = outputApi; + + const ui = useSimulationUi(); + + return ( + <> + + + setActiveOutputTab(v as any)} + className="h-full flex flex-col" + > +
+ + openOutputPanel("compiler")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + Compiler + + openOutputPanel("messages")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + Messages + + openOutputPanel("registry")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + I/O Registry + + + {ui.debugMode && ( + openOutputPanel("debug")} + className={"h-[var(--ui-button-height)] px-2 text-ui-xs data-[state=active]:bg-background rounded-sm py-0 leading-none flex items-center"} + > + Telemetry + + )} + + +
+
+ +
+
+ + + + + + + setParserPanelDismissed(true)} hideHeader={true} /> + + + + {}} hideHeader={true} defaultTab="registry" /> + + + {ui.debugMode && ( + + {/* Telemetry / Debug console moved back into the output tabs */} + + + )} + + + + + ); +} + diff --git a/client/src/hooks/use-compilation.ts b/client/src/hooks/use-compilation.ts index f54ea1c3..ce1a9418 100644 --- a/client/src/hooks/use-compilation.ts +++ b/client/src/hooks/use-compilation.ts @@ -12,12 +12,7 @@ type CliStatus = "idle" | "compiling" | "success" | "error"; type SetState = (value: T | ((prev: T) => T)) => void; -type DebugMessageParams = { - source: "frontend" | "server"; - type: string; - data: string; - protocol?: "websocket" | "http"; -}; + type CompilePayload = { code: string; @@ -37,8 +32,6 @@ type UseCompilationParams = { setIoRegistry: SetState; setHasCompiledOnce: SetState; setIsModified: SetState; - setDebugMessages: SetState; - addDebugMessage: (params: DebugMessageParams) => void; ensureBackendConnected: (reason: string) => boolean; isBackendUnreachableError: (error: unknown) => boolean; triggerErrorGlitch: () => void; @@ -50,6 +43,8 @@ type UseCompilationParams = { startSimulation: () => void; }; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; + export function useCompilation({ editorRef, tabs, @@ -63,14 +58,14 @@ export function useCompilation({ setIoRegistry, setHasCompiledOnce, setIsModified, - setDebugMessages, - addDebugMessage, ensureBackendConnected, isBackendUnreachableError, triggerErrorGlitch, toast, startSimulation, }: UseCompilationParams) { + // pull debug helpers from UI context (provider owns debug state) + const { addDebugMessage, setDebugMessages } = useSimulationUi(); const [compilationStatus, setCompilationStatus] = useState( "ready", ); @@ -94,16 +89,16 @@ export function useCompilation({ const uploadMutation = useMutation({ mutationFn: async (payload: CompilePayload) => { - addDebugMessage({ - source: "frontend", - type: "upload_request", - data: JSON.stringify( + addDebugMessage?.( + "frontend", + "upload_request", + JSON.stringify( { endpoint: "POST /api/upload", codeLength: payload.code.length }, null, 2, ), - protocol: "http", - }); + "http", + ); const response = await apiRequest("POST", "/api/upload", payload); const ct = (response.headers.get("content-type") || "").toLowerCase(); if (ct.includes("application/json")) { @@ -166,16 +161,16 @@ export function useCompilation({ mutationFn: async (payload: CompilePayload) => { setArduinoCliStatus("compiling"); setLastCompilationResult(null); - addDebugMessage({ - source: "frontend", - type: "compile_request", - data: JSON.stringify( + addDebugMessage?.( + "frontend", + "compile_request", + JSON.stringify( { endpoint: "POST /api/compile", codeLength: payload.code.length }, null, 2, ), - protocol: "http", - }); + "http", + ); const response = await apiRequest("POST", "/api/compile", payload); const ct = (response.headers.get("content-type") || "").toLowerCase(); if (ct.includes("application/json")) { @@ -195,34 +190,34 @@ export function useCompilation({ setHasCompilationErrors(false); setLastCompilationResult("success"); setCliOutput(data.output || "✓ Arduino-CLI Compilation succeeded."); - addDebugMessage({ - source: "server", - type: "compilation_status", - data: JSON.stringify({ gccStatus: "success" }, null, 2), - protocol: "http", - }); + addDebugMessage?.( + "server", + "compilation_status", + JSON.stringify({ gccStatus: "success" }, null, 2), + "http", + ); } else { setArduinoCliStatus("error"); setHasCompilationErrors(true); setLastCompilationResult("error"); triggerErrorGlitch(); setCliOutput(data.errors || "✗ Arduino-CLI Compilation failed."); - addDebugMessage({ - source: "server", - type: "compilation_error", - data: JSON.stringify( + addDebugMessage?.( + "server", + "compilation_error", + JSON.stringify( { type: "compilation_error", data: data.errors }, null, 2, ), - protocol: "http", - }); - addDebugMessage({ - source: "server", - type: "compilation_status", - data: JSON.stringify({ gccStatus: "error" }, null, 2), - protocol: "http", - }); + "http", + ); + addDebugMessage?.( + "server", + "compilation_status", + JSON.stringify({ gccStatus: "error" }, null, 2), + "http", + ); } if (data.parserMessages && Array.isArray(data.parserMessages)) { @@ -268,7 +263,7 @@ export function useCompilation({ } } } catch (err) { - console.error("Error handling post-compile upload", err); + logger.error(`Error handling post-compile upload: ${String(err)}`); } }, onError: (error) => { @@ -326,14 +321,15 @@ export function useCompilation({ const handleCompileAndStart = useCallback(() => { if (!ensureBackendConnected("Simulation starten")) return; - setDebugMessages([]); +// clear debug messages via provider + setDebugMessages?.([]); let mainSketchCode: string = ""; if (editorRef.current) { try { mainSketchCode = editorRef.current.getValue(); } catch (error) { - console.error("[CLIENT] Error getting code from editor:", error); + logger.error(`[CLIENT] Error getting code from editor: ${String(error)}`); } } diff --git a/client/src/hooks/use-simulation-controls.ts b/client/src/hooks/use-simulation-controls.ts index e7b43a5c..6f456a9f 100644 --- a/client/src/hooks/use-simulation-controls.ts +++ b/client/src/hooks/use-simulation-controls.ts @@ -6,19 +6,14 @@ type SimulationStatus = "running" | "stopped" | "paused"; type SetState = (value: T | ((prev: T) => T)) => void; -type DebugMessageParams = { - source: "frontend" | "server"; - type: string; - data: string; - protocol?: "websocket" | "http"; -}; + type UseSimulationControlsParams = { ensureBackendConnected: (reason: string) => boolean; sendMessage: (message: any) => void; + sendMessageImmediate?: (message: any) => void; resetPinUI: (opts?: { keepDetected?: boolean }) => void; clearOutputs: () => void; - addDebugMessage: (params: DebugMessageParams) => void; serialEventQueueRef: MutableRefObject< Array<{ payload: any; receivedAt: number }> >; @@ -35,12 +30,14 @@ type UseSimulationControlsParams = { startSimulationRef: MutableRefObject<(() => void) | null>; }; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; + export function useSimulationControls({ ensureBackendConnected, sendMessage, + sendMessageImmediate, resetPinUI, clearOutputs, - addDebugMessage, serialEventQueueRef, toast, pendingPinConflicts, @@ -50,6 +47,7 @@ export function useSimulationControls({ handleCompileAndStart, startSimulationRef, }: UseSimulationControlsParams) { + const { addDebugMessage } = useSimulationUi(); const [simulationStatus, setSimulationStatus] = useState( "stopped", ); @@ -62,13 +60,15 @@ export function useSimulationControls({ const stopMutation = useMutation({ mutationFn: async () => { - addDebugMessage({ - source: "frontend", - type: "stop_simulation", - data: JSON.stringify({ type: "stop_simulation" }, null, 2), - protocol: "websocket", - }); - sendMessage({ type: "stop_simulation" }); + addDebugMessage?.( + "frontend", + "stop_simulation", + JSON.stringify({ type: "stop_simulation" }, null, 2), + "websocket", + ); + // use immediate send for stop to avoid buffered delays + if (sendMessageImmediate) sendMessageImmediate({ type: "stop_simulation" }); + else sendMessage({ type: "stop_simulation" }); return { success: true }; }, onSuccess: () => { @@ -80,13 +80,14 @@ export function useSimulationControls({ const pauseMutation = useMutation({ mutationFn: async () => { - addDebugMessage({ - source: "frontend", - type: "pause_simulation", - data: JSON.stringify({ type: "pause_simulation" }, null, 2), - protocol: "websocket", - }); - sendMessage({ type: "pause_simulation" }); + addDebugMessage?.( + "frontend", + "pause_simulation", + JSON.stringify({ type: "pause_simulation" }, null, 2), + "websocket", + ); + if (sendMessageImmediate) sendMessageImmediate({ type: "pause_simulation" }); + else sendMessage({ type: "pause_simulation" }); return { success: true }; }, onSuccess: () => { @@ -103,13 +104,14 @@ export function useSimulationControls({ const resumeMutation = useMutation({ mutationFn: async () => { - addDebugMessage({ - source: "frontend", - type: "resume_simulation", - data: JSON.stringify({ type: "resume_simulation" }, null, 2), - protocol: "websocket", - }); - sendMessage({ type: "resume_simulation" }); + addDebugMessage?.( + "frontend", + "resume_simulation", + JSON.stringify({ type: "resume_simulation" }, null, 2), + "websocket", + ); + if (sendMessageImmediate) sendMessageImmediate({ type: "resume_simulation" }); + else sendMessage({ type: "resume_simulation" }); return { success: true }; }, onSuccess: () => { @@ -127,16 +129,16 @@ export function useSimulationControls({ const startMutation = useMutation({ mutationFn: async () => { resetPinUI({ keepDetected: true }); - addDebugMessage({ - source: "frontend", - type: "start_simulation", - data: JSON.stringify( + addDebugMessage?.( + "frontend", + "start_simulation", + JSON.stringify( { type: "start_simulation", timeout: simulationTimeout }, null, 2, ), - protocol: "websocket", - }); + "websocket", + ); sendMessage({ type: "start_simulation", timeout: simulationTimeout }); return { success: true }; }, @@ -200,7 +202,8 @@ export function useSimulationControls({ const handleReset = useCallback(() => { if (!ensureBackendConnected("Reset simulation")) return; if (simulationStatus === "running") { - sendMessage({ type: "stop_simulation" }); + if (sendMessageImmediate) sendMessageImmediate({ type: "stop_simulation" }); + else sendMessage({ type: "stop_simulation" }); setSimulationStatus("stopped"); } clearOutputs(); diff --git a/client/src/hooks/use-simulation-lifecycle.ts b/client/src/hooks/use-simulation-lifecycle.ts index 499451d9..febf4601 100644 --- a/client/src/hooks/use-simulation-lifecycle.ts +++ b/client/src/hooks/use-simulation-lifecycle.ts @@ -5,6 +5,7 @@ export interface UseSimulationLifecycleOptions { simulationStatus: string; setSimulationStatus: (s: any) => void; sendMessage: (msg: any) => void; + sendMessageImmediate?: (msg: any) => void; resetPinUI: (opts?: { keepDetected?: boolean }) => void; clearOutputs?: () => void; handlePause?: () => void; @@ -18,6 +19,7 @@ export function useSimulationLifecycle({ simulationStatus, setSimulationStatus, sendMessage, + sendMessageImmediate, resetPinUI, clearOutputs, handlePause, @@ -41,7 +43,8 @@ export function useSimulationLifecycle({ const stopSimulation = useCallback(() => { try { - sendMessage({ type: "stop_simulation" }); + if ((sendMessageImmediate as any) != null) sendMessageImmediate?.({ type: "stop_simulation" }); + else sendMessage({ type: "stop_simulation" }); } catch {} try { setSimulationStatus("stopped"); @@ -49,7 +52,7 @@ export function useSimulationLifecycle({ try { resetPinUI(); } catch {} - }, [sendMessage, setSimulationStatus, resetPinUI]); + }, [sendMessage, sendMessageImmediate, setSimulationStatus, resetPinUI]); // Watch for code edits and stop running/paused simulation (unless suppressed) useEffect(() => { diff --git a/client/src/hooks/use-simulation-store.ts b/client/src/hooks/use-simulation-store.ts index cd2547d5..5b2442f7 100644 --- a/client/src/hooks/use-simulation-store.ts +++ b/client/src/hooks/use-simulation-store.ts @@ -1,4 +1,6 @@ import { useSyncExternalStore } from "react"; +import { Logger } from "@shared/logger"; +const logger = new Logger("use-simulation-store"); type PinMode = "INPUT" | "OUTPUT" | "INPUT_PULLUP"; export type PinStateType = "mode" | "value" | "pwm"; @@ -235,10 +237,10 @@ if (typeof window !== "undefined") { const { telemetryStore } = await import('./use-telemetry-store'); telemetryStore.resetToInitial(); } catch (err) { - console.warn('[SIM_DEBUG] Could not reset telemetry store:', err); + logger.warn(`[SIM_DEBUG] Could not reset telemetry store: ${String(err)}`); } }, - }; + }; } export const useSimulationStore = () => { diff --git a/client/src/hooks/use-simulation-ui.tsx b/client/src/hooks/use-simulation-ui.tsx new file mode 100644 index 00000000..240bd7a7 --- /dev/null +++ b/client/src/hooks/use-simulation-ui.tsx @@ -0,0 +1,186 @@ +import React, { createContext, useContext, useState, useRef } from "react"; + +import { useDebugConsole } from "@/hooks/use-debug-console"; +import { useOutputPanel } from "@/hooks/use-output-panel"; + +type SimulationStatus = "running" | "paused" | "stopped"; + +type DebugMessage = { + id: string; + timestamp: Date; + sender: "server" | "frontend"; + type: string; + content: string; + protocol?: "websocket" | "http"; +}; + +type SimulationUiContextType = { + simulationStatus: SimulationStatus; + txActivity: number; + rxActivity: number; + setTxActivity?: React.Dispatch>; + setRxActivity?: React.Dispatch>; + + // Serial I/O (optional — page provides) + serialOutput?: any[]; + renderedSerialOutput?: any[]; + serialViewMode?: "monitor" | "plotter" | "both"; + autoScrollEnabled?: boolean; + setAutoScrollEnabled?: (v: boolean) => void; + serialInputValue?: string; + setSerialInputValue?: (v: string) => void; + showSerialMonitor?: boolean; + showSerialPlotter?: boolean; + cycleSerialViewMode?: () => void; + clearSerialOutput?: () => void; + + // Output panel state (managed by provider) + activeOutputTab?: "compiler" | "messages" | "registry" | "debug"; + setActiveOutputTab?: (v: any) => void; + showCompilationOutput?: boolean; + setShowCompilationOutput?: (v: boolean) => void; + parserPanelDismissed?: boolean; + setParserPanelDismissed?: (v: boolean) => void; + parserMessagesContainerRef?: React.RefObject; + outputPanelRef?: any; + outputTabsHeaderRef?: React.RefObject; + outputPanelMinPercent?: number; + compilationPanelSize?: number; + setCompilationPanelSize?: (n: number) => void; + outputPanelManuallyResizedRef?: React.MutableRefObject; + openOutputPanel?: (tab: any) => void; + + // Debug console (provided by provider) + debugMode?: boolean; + setDebugMode?: (v: boolean) => void; + debugMessages?: DebugMessage[]; + setDebugMessages?: (msgs: DebugMessage[]) => void; + addDebugMessage?: (sender: "server" | "frontend", type: string, content: string, protocol?: "websocket" | "http") => void; + debugMessageFilter?: string; + setDebugMessageFilter?: (v: string) => void; + debugViewMode?: "table" | "tiles"; + setDebugViewMode?: (v: "table" | "tiles") => void; + debugMessagesContainerRef?: React.RefObject; +}; + +const SimulationUiContext = createContext(null); + +export const SimulationUiProvider = (props: React.PropsWithChildren & { + // data the provider needs from the page to drive output sizing + cliOutput?: string; + parserMessages?: any[]; + lastCompilationResult?: "success" | "error" | null; + hasCompilationErrors?: boolean; + code?: string; +}>) => { + const { children, cliOutput = "", parserMessages = [], lastCompilationResult = null, hasCompilationErrors = false, code = "", ...rest } = props as any; + + // Output panel state managed by provider so multiple components can consume it + const [activeOutputTab, setActiveOutputTab] = useState<"compiler" | "messages" | "registry" | "debug">("compiler"); + const [showCompilationOutput, setShowCompilationOutput] = useState(() => { + try { + const stored = typeof window !== "undefined" ? window.localStorage.getItem("unoShowCompileOutput") : null; + return stored === null ? true : stored === "1"; + } catch { + return true; + } + }); + const [parserPanelDismissed, setParserPanelDismissed] = useState(false); + const parserMessagesContainerRef = useRef(null); + + // Hook that manages the output panel sizing/behavior + const outputPanel = useOutputPanel( + Boolean(hasCompilationErrors), + cliOutput, + parserMessages, + lastCompilationResult ?? null, + parserMessagesContainerRef, + showCompilationOutput, + setShowCompilationOutput, + setParserPanelDismissed, + setActiveOutputTab, + code, + ); + + // Debug console is owned by the provider now so pages/components don't + // need to instantiate or forward debug state. We pass the current + // active output tab to the debug-hook so scroll/auto-open behavior + // continues to work as before. + const { + debugMode, + setDebugMode, + debugMessages, + setDebugMessages, + debugMessageFilter, + setDebugMessageFilter, + debugViewMode, + setDebugViewMode, + debugMessagesContainerRef, + addDebugMessage, + } = useDebugConsole(activeOutputTab); + + + const contextValue: SimulationUiContextType = { + // Spread any serial/debug values passed in by the page (keeps backward compat) + ...(rest as SimulationUiContextType), + + // Output panel + activeOutputTab, + setActiveOutputTab, + showCompilationOutput, + setShowCompilationOutput, + parserPanelDismissed, + setParserPanelDismissed, + parserMessagesContainerRef, + outputPanelRef: outputPanel.outputPanelRef, + outputTabsHeaderRef: outputPanel.outputTabsHeaderRef, + outputPanelMinPercent: outputPanel.outputPanelMinPercent, + compilationPanelSize: outputPanel.compilationPanelSize, + setCompilationPanelSize: outputPanel.setCompilationPanelSize, + outputPanelManuallyResizedRef: outputPanel.outputPanelManuallyResizedRef, + openOutputPanel: outputPanel.openOutputPanel, + + // debug console (managed by provider) + debugMode, + setDebugMode, + debugMessages, + setDebugMessages, + addDebugMessage, + debugMessageFilter, + setDebugMessageFilter, + debugViewMode, + setDebugViewMode, + debugMessagesContainerRef, + + // minimal tx/rx defaults (may be overridden by page props) + txActivity: (rest as any)?.txActivity ?? 0, + rxActivity: (rest as any)?.rxActivity ?? 0, + setTxActivity: (rest as any)?.setTxActivity, + setRxActivity: (rest as any)?.setRxActivity, + + // serial fields (if provided by page via props) + serialOutput: (rest as any)?.serialOutput, + renderedSerialOutput: (rest as any)?.renderedSerialOutput, + serialViewMode: (rest as any)?.serialViewMode, + autoScrollEnabled: (rest as any)?.autoScrollEnabled, + setAutoScrollEnabled: (rest as any)?.setAutoScrollEnabled, + serialInputValue: (rest as any)?.serialInputValue, + setSerialInputValue: (rest as any)?.setSerialInputValue, + showSerialMonitor: (rest as any)?.showSerialMonitor, + showSerialPlotter: (rest as any)?.showSerialPlotter, + cycleSerialViewMode: (rest as any)?.cycleSerialViewMode, + clearSerialOutput: (rest as any)?.clearSerialOutput, + } as SimulationUiContextType; + + return ( + {children} + ); +}; + +export function useSimulationUi() { + const ctx = useContext(SimulationUiContext); + if (!ctx) { + throw new Error("useSimulationUi must be used within a SimulationUiProvider"); + } + return ctx; +} diff --git a/client/src/hooks/useWebSocketHandler.ts b/client/src/hooks/useWebSocketHandler.ts index 39a38e10..f8c90e47 100644 --- a/client/src/hooks/useWebSocketHandler.ts +++ b/client/src/hooks/useWebSocketHandler.ts @@ -16,8 +16,7 @@ type UseWebSocketHandlerParams = { simulationStatus: string; // callbacks / setters from parent scope - addDebugMessage: (source: "frontend" | "server", type: string, data: string, protocol?: "websocket" | "http") => void; - setRxActivity: React.Dispatch>; + setRxActivity?: React.Dispatch>; appendSerialOutput: (text: string) => void; appendRenderedText: (text: string) => void; setSerialOutput: React.Dispatch>; @@ -51,10 +50,11 @@ type UseWebSocketHandlerParams = { setParserMessages: React.Dispatch>; }; +import { useSimulationUi } from "@/hooks/use-simulation-ui"; + export function useWebSocketHandler(params: UseWebSocketHandlerParams) { const { simulationStatus, - addDebugMessage, setRxActivity, appendSerialOutput, appendRenderedText, @@ -84,6 +84,9 @@ export function useWebSocketHandler(params: UseWebSocketHandlerParams) { setParserMessages, } = params; + // get debug function from UI context + const { addDebugMessage } = useSimulationUi(); + const { isConnected, messageQueue, @@ -101,9 +104,12 @@ export function useWebSocketHandler(params: UseWebSocketHandlerParams) { const processMessage = (message: any) => { switch (message.type) { case "sim_telemetry": { - if (simulationStatus === "running") { - telemetryStore.pushTelemetry((message as any).metrics); - } + // Always forward telemetry messages to the telemetry store. Previously + // this was gated on `simulationStatus === "running"` which could + // miss telemetry when messages arrive before the local status is set. + const metrics = (message as any).metrics; + telemetryStore.pushTelemetry(metrics); + logger.debug(`[useWebSocketHandler] sim_telemetry received — ts=${metrics?.timestamp} batches=${metrics?.batchesPerSecond}`); break; } case "serial_output": { @@ -114,7 +120,7 @@ export function useWebSocketHandler(params: UseWebSocketHandlerParams) { break; } - setRxActivity((prev) => prev + 1); + setRxActivity?.((prev) => prev + 1); const isNewlineOnly = text === "\n" || text === "\r\n"; if (isNewlineOnly) text = ""; @@ -359,7 +365,7 @@ export function useWebSocketHandler(params: UseWebSocketHandlerParams) { // Log all messages to debug console BEFORE consuming them messageQueue.forEach((msg) => { - addDebugMessage("server", msg.type || "unknown", JSON.stringify(msg, null, 2), "websocket"); + addDebugMessage?.("server", msg.type || "unknown", JSON.stringify(msg, null, 2), "websocket"); }); const messages = consumeMessages(); diff --git a/client/src/lib/debugBridge.ts b/client/src/lib/debugBridge.ts new file mode 100644 index 00000000..e69de29b diff --git a/client/src/lib/font-scale-utils.ts b/client/src/lib/font-scale-utils.ts index 117d6cde..03b98b86 100644 --- a/client/src/lib/font-scale-utils.ts +++ b/client/src/lib/font-scale-utils.ts @@ -1,5 +1,8 @@ // font-scale-utils.ts - Utility functions for global font scale management +import { Logger } from "@shared/logger"; +const logger = new Logger("font-scale-utils"); + export const FONT_SCALES = [ { label: "S", value: 0.875, px: 12 }, { label: "M", value: 1.0, px: 14 }, @@ -30,7 +33,7 @@ export function setFontScale(scale: number): void { new CustomEvent("uiFontScaleChange", { detail: { value: scale } }) ); } catch (e) { - console.error("Failed to set font scale:", e); + logger.error(`Failed to set font scale: ${String(e)}`); } } diff --git a/client/src/pages/arduino-simulator.tsx b/client/src/pages/arduino-simulator.tsx index f29cb694..353fdd8d 100644 --- a/client/src/pages/arduino-simulator.tsx +++ b/client/src/pages/arduino-simulator.tsx @@ -1,42 +1,22 @@ //arduino-simulator.tsx -import { - useState, - useEffect, - useRef, - useCallback, - lazy, - Suspense, -} from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { - Cpu, - Terminal, - Wrench, - Trash2, - ChevronsDown, - BarChart, - Monitor, - Columns, - X, - Table, - LayoutGrid, -} from "lucide-react"; -import { InputGroup } from "@/components/ui/input-group"; +import { Cpu, Terminal, Wrench, Monitor } from "lucide-react"; + import { clsx } from "clsx"; import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { CodeEditor } from "@/components/features/code-editor"; import { SerialMonitor } from "@/components/features/serial-monitor"; import { CompilationOutput } from "@/components/features/compilation-output"; import { ParserOutput } from "@/components/features/parser-output"; import { SketchTabs } from "@/components/features/sketch-tabs"; import { ExamplesMenu } from "@/components/features/examples-menu"; -import { ArduinoBoard } from "@/components/features/arduino-board"; -import { PinMonitor } from "@/components/features/pin-monitor"; import { AppHeader } from "@/components/features/app-header"; import { SimCockpit } from "@/components/features/sim-cockpit"; +import SimulatorSidebar, { SimulatorOutput } from "@/components/features/simulator/SimulatorSidebar"; +import SimulatorOutputPanel from "@/components/features/simulator/SimulatorOutputPanel"; import { useWebSocket } from "@/hooks/use-websocket"; import { useWebSocketHandler } from "@/hooks/useWebSocketHandler"; import { useCompilation } from "@/hooks/use-compilation"; @@ -45,14 +25,17 @@ import { usePinState } from "@/hooks/use-pin-state"; import { useToast } from "@/hooks/use-toast"; import { useBackendHealth } from "@/hooks/use-backend-health"; import { useMobileLayout } from "@/hooks/use-mobile-layout"; -import { useDebugConsole } from "@/hooks/use-debug-console"; + import { useDebugMode } from "@/hooks/use-debug-mode-store"; import { useSketchTabs } from "@/hooks/use-sketch-tabs"; import { useSerialIO } from "@/hooks/use-serial-io"; import { useOutputPanel } from "@/hooks/use-output-panel"; import { useSimulationStore } from "@/hooks/use-simulation-store"; +import { SimulationUiProvider, useSimulationUi } from "@/hooks/use-simulation-ui"; +import SimulatorHeader from "@/components/features/simulator/SimulatorHeader"; + import { useSketchAnalysis } from "@/hooks/use-sketch-analysis"; -import { useTelemetryStore } from "@/hooks/use-telemetry-store"; + import { useFileManager } from "@/hooks/use-file-manager"; import { useSimulationLifecycle } from "@/hooks/use-simulation-lifecycle"; import { @@ -60,7 +43,7 @@ import { ResizablePanel, ResizableHandle, } from "@/components/ui/resizable"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; + import type { Sketch, ParserMessage, @@ -68,25 +51,13 @@ import type { } from "@shared/schema"; import { isMac } from "@/lib/platform"; -// Lazy load SerialPlotter to defer recharts (~400KB) until needed -const SerialPlotter = lazy(() => - import("@/components/features/serial-plotter").then((m) => ({ - default: m.SerialPlotter, - })), -); -// Loading placeholder for lazy components -const LoadingPlaceholder = () => ( -
- Loading chart... -
-); // Logger import import { Logger } from "@shared/logger"; const logger = new Logger("ArduinoSimulator"); -export default function ArduinoSimulator() { +function ArduinoSimulatorInner() { const [currentSketch, setCurrentSketch] = useState(null); const [code, setCode] = useState(""); const editorRef = useRef<{ getValue: () => string } | null>(null); @@ -121,20 +92,6 @@ export default function ArduinoSimulator() { // Track if user manually dismissed the parser panel (reset on new compile with messages) const [parserPanelDismissed, setParserPanelDismissed] = useState(false); - // Initialize I/O Registry with all 20 Arduino pins (will be populated at runtime) - const [ioRegistry, setIoRegistry] = useState(() => { - const pins: IOPinRecord[] = []; - // Digital pins 0-13 - for (let i = 0; i <= 13; i++) { - pins.push({ pin: String(i), defined: false, usedAt: [] }); - } - // Analog pins A0-A5 - for (let i = 0; i <= 5; i++) { - pins.push({ pin: `A${i}`, defined: false, usedAt: [] }); - } - return pins; - }); - const [activeOutputTab, setActiveOutputTab] = useState< "compiler" | "messages" | "registry" | "debug" >("compiler"); @@ -148,16 +105,36 @@ export default function ArduinoSimulator() { } }, ); + + // --- Debug proxies (provider owns the real state) + // Hooks below will emit debug messages via the bridge (see `debugBridge`). + + + + // Initialize I/O Registry with all 20 Arduino pins (will be populated at runtime) + const [ioRegistry, setIoRegistry] = useState(() => { + const pins: IOPinRecord[] = []; + // Digital pins 0-13 + for (let i = 0; i <= 13; i++) { + pins.push({ pin: String(i), defined: false, usedAt: [] }); + } + // Analog pins A0-A5 + for (let i = 0; i <= 5; i++) { + pins.push({ pin: `A${i}`, defined: false, usedAt: [] }); + } + return pins; + }); const [isModified, setIsModified] = useState(false); const { - pinStates, setPinStates, resetPinStates, enqueuePinEvent, batchStats, } = useSimulationStore(); + const { setTxActivity, txActivity, rxActivity } = useSimulationUi(); + // Pin state management via hook const { analogPinsUsed, @@ -181,23 +158,9 @@ export default function ArduinoSimulator() { // File manager hook — instantiated after `handleFilesLoaded` to avoid TDZ (see below) - // Debug console state and functions - const { - debugMode, - setDebugMode: _setDebugMode, - debugMessages, - setDebugMessages, - debugMessageFilter, - setDebugMessageFilter, - debugViewMode, - setDebugViewMode, - debugMessagesContainerRef, - addDebugMessage, - } = useDebugConsole(activeOutputTab); - void _setDebugMode; // Mark as intentionally unused (managed by hook) // Subscribe to telemetry updates (to re-render when metrics change) - const telemetryData = useTelemetryStore(); + // Helper to request the global Settings dialog to open (App listens for this event) const openSettings = () => { @@ -206,19 +169,12 @@ export default function ArduinoSimulator() { } catch {} }; - const handleSerialInputSend = () => { - if (!serialInputValue.trim()) return; - handleSerialSend(serialInputValue); - setSerialInputValue(""); - }; - const handleSerialInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") handleSerialInputSend(); - }; + + // RX/TX LED activity counters (increment on activity for change detection) - const [txActivity, setTxActivity] = useState(0); - const [rxActivity, setRxActivity] = useState(0); + // Queue for incoming serial_events - use ref to avoid React batching issues const serialEventQueueRef = useRef< Array<{ payload: any; receivedAt: number }> @@ -259,7 +215,7 @@ export default function ArduinoSimulator() { : "Telemetry displays are now hidden", }); } catch (err) { - console.error("Failed to toggle debug mode:", err); + logger.error(`Failed to toggle debug mode: ${String(err)}`); } } }; @@ -272,6 +228,7 @@ export default function ArduinoSimulator() { isConnected, lastMessage, sendMessage: sendMessageRaw, + sendMessageImmediate, } = useWebSocket(); // Mark some hook values as intentionally read to avoid TS unused-local errors void lastMessage; @@ -336,14 +293,6 @@ export default function ArduinoSimulator() { setIoRegistry, setHasCompiledOnce: setHasCompiledOnceProxy, setIsModified, - setDebugMessages, - addDebugMessage: (params) => - addDebugMessage( - params.source, - params.type, - params.data, - params.protocol, - ), ensureBackendConnected, isBackendUnreachableError, triggerErrorGlitch, @@ -351,6 +300,28 @@ export default function ArduinoSimulator() { startSimulation, }); + // Output panel refs / sizing (kept here for AppHeader & legacy `SimulatorOutput` props) + const { + outputPanelRef, + outputTabsHeaderRef, + outputPanelMinPercent, + compilationPanelSize, + setCompilationPanelSize, + outputPanelManuallyResizedRef, + openOutputPanel, + } = useOutputPanel( + Boolean(hasCompilationErrors), + cliOutput, + parserMessages, + lastCompilationResult, + parserMessagesContainerRef, + showCompilationOutput, + setShowCompilationOutput, + setParserPanelDismissed, + setActiveOutputTab, + code, + ); + const { simulationStatus, setSimulationStatus, @@ -368,15 +339,9 @@ export default function ArduinoSimulator() { } = useSimulationControls({ ensureBackendConnected, sendMessage, + sendMessageImmediate, resetPinUI, clearOutputs, - addDebugMessage: (params) => - addDebugMessage( - params.source, - params.type, - params.data, - params.protocol, - ), serialEventQueueRef, toast, pendingPinConflicts, @@ -390,11 +355,12 @@ export default function ArduinoSimulator() { setHasCompiledOnceRef.current = setHasCompiledOnce; // Simulation lifecycle orchestration (auto-stop on code edits / compiler errors) - const { suppressAutoStopOnce } = useSimulationLifecycle({ + useSimulationLifecycle({ code, simulationStatus, setSimulationStatus, sendMessage, + sendMessageImmediate, resetPinUI, clearOutputs, handlePause, @@ -403,45 +369,12 @@ export default function ArduinoSimulator() { hasCompilationErrors, }); - // Output panel sizing and management - const { - outputPanelRef, - outputTabsHeaderRef, - compilationPanelSize, - setCompilationPanelSize, - outputPanelMinPercent, - outputPanelManuallyResizedRef, - openOutputPanel, - } = useOutputPanel( - hasCompilationErrors, - cliOutput, - parserMessages, - lastCompilationResult, - parserMessagesContainerRef, - showCompilationOutput, - setShowCompilationOutput, - setParserPanelDismissed, - setActiveOutputTab, - code, - ); + // Output panel sizing/logic and activeOutputTab are now owned by + // `SimulationUiProvider` (consumed by `useSimulationUi()`). This removes + // prop drilling and centralizes output behavior. + - // Auto-switch output tab based on errors and messages - useEffect(() => { - if (hasCompilationErrors) { - setActiveOutputTab("compiler"); - } else if (parserMessages.length > 0 && !parserPanelDismissed) { - setActiveOutputTab("messages"); - } - }, [hasCompilationErrors, parserMessages.length, parserPanelDismissed]); - // Auto-scroll debug console to latest message - useEffect(() => { - if (activeOutputTab === "debug" && debugMessagesContainerRef.current) { - requestAnimationFrame(() => { - debugMessagesContainerRef.current?.scrollTo(0, debugMessagesContainerRef.current.scrollHeight); - }); - } - }, [debugMessages, activeOutputTab]); // Fetch default sketch const { data: sketches } = useQuery({ @@ -613,7 +546,7 @@ export default function ArduinoSimulator() { try { ed[cmd](); } catch (err) { - console.error("Editor command failed", err); + logger.error(`Editor command failed: ${String(err)}`); } } else { toast({ @@ -636,7 +569,7 @@ export default function ArduinoSimulator() { try { ed.copy(); } catch (err) { - console.error("Copy failed", err); + logger.error(`Copy failed: ${String(err)}`); } }; @@ -653,7 +586,7 @@ export default function ArduinoSimulator() { try { ed.cut(); } catch (err) { - console.error("Cut failed", err); + logger.error(`Cut failed: ${String(err)}`); } }; @@ -670,7 +603,7 @@ export default function ArduinoSimulator() { try { ed.paste(); } catch (err) { - console.error("Paste failed", err); + logger.error(`Paste failed: ${String(err)}`); } }; @@ -697,15 +630,13 @@ export default function ArduinoSimulator() { try { ed.goToLine(num); } catch (err) { - console.error("Go to line failed", err); + logger.error(`Go to line failed: ${String(err)}`); } }; // WebSocket message handling moved to `useWebSocketHandler` (extracted for better separation of concerns) useWebSocketHandler({ simulationStatus, - addDebugMessage, - setRxActivity, appendSerialOutput, appendRenderedText, setSerialOutput, @@ -889,9 +820,9 @@ export default function ArduinoSimulator() { replaceAll: boolean, ) => { if (replaceAll) { - // Stop simulation if running + // Stop simulation if running — use central handler (ensures backend connectivity) if (simulationStatus === "running") { - sendMessage({ type: "stop_simulation" }); + handleStop(); } // Replace all tabs with new files @@ -952,7 +883,7 @@ export default function ArduinoSimulator() { const handleLoadExample = (filename: string, content: string) => { // Stop simulation if running if (simulationStatus === "running") { - sendMessage({ type: "stop_simulation" }); + handleStop(); } // Create a new sketch from the example, using the filename as the tab name @@ -1107,7 +1038,7 @@ export default function ArduinoSimulator() { } // Trigger TX LED blink when client sends data - setTxActivity((prev) => prev + 1); + setTxActivity?.((prev: number | undefined) => (prev ?? 0) + 1); sendMessage({ type: "serial_input", @@ -1192,21 +1123,48 @@ export default function ArduinoSimulator() { void stopDisabled; void buttonsClassName; + function HeaderRightSlot() { + const { debugMode } = useSimulationUi(); + return ( +
+ { if (!compileMutation.isPending) handleCompile(); }} + onCompileAndStart={handleCompileAndStart} + board={board} + /> + {debugMode ? : null} +
+ ); + } + return (
- {/* Glitch overlay when compilation fails */} - {showErrorGlitch && ( -
- {/* Single red border flash */} -
-
-
-
-
+ {/* Glitch overlay when compilation fails */} + {showErrorGlitch && ( +
+ {/* Single red border flash */} +
+
+
+
+