polish(sim): route circuit faults to the output console instead of a toolbar toast

The runtime burnout / pre-flight messages used an inline `setMessage` toast
that rendered as a bar near the Run/Stop buttons and overlapped them. Route
all circuit findings into the compile output console instead — one unified,
red/orange diagnostics log next to the compiler output (Proteus-style):

- New "Circuit check" console group (CIRCUIT_CHECK_TARGET). checkOrBlock logs
  every error (red) / warning (orange) there and opens the console; the
  blocking modal is kept for the explicit Run-anyway / Cancel decision.
- Runtime `velxio-circuit-fault` events (LED burnout) log to the same group
  instead of the toast; no auto-open (the continuous solver can fault on load).
- Warnings-only no longer pop a toast — the console entry is the record.
- The run-path clears preserve circuit-check entries so findings survive a
  "Run anyway" auto-compile.
This commit is contained in:
David Montero 2026-06-17 21:46:04 +02:00
parent f7f0eb4ba8
commit 9ec48d5021
1 changed files with 65 additions and 24 deletions

View File

@ -38,6 +38,19 @@ import {
} from '../../utils/analytics';
import './EditorToolbar.css';
/**
* Output-console group for circuit pre-flight + runtime faults. Routing these
* into the compile console (instead of an inline toolbar toast that overlapped
* the Run/Stop buttons) gives one unified, red-coloured diagnostics log
* Proteus-style. id is matched when clearing so the findings survive an
* auto-compile triggered by the same Run.
*/
const CIRCUIT_CHECK_TARGET: CompileTarget = {
id: 'circuit-check',
label: 'Circuit check',
kind: 'board',
};
/**
* Clear the output drives of every custom chip on the canvas and re-solve, so
* chip-driven LEDs go dark on Stop. A chip drives its nets via its own SPICE
@ -243,15 +256,25 @@ export const EditorToolbar = ({
}, []);
// Surface a runtime circuit fault (e.g. an LED that burnt out from
// overcurrent during the live SPICE solve) as an inline message.
// overcurrent during the live SPICE solve) in the output console, in red,
// under the "Circuit check" group — same place as the pre-flight findings.
// (Previously an inline toolbar toast that overlapped the Run/Stop buttons.)
// We do NOT auto-open the console here: the continuous solver can fault on
// load, and popping the console open then would be intrusive. The pre-flight
// (on Run) opens it; this entry then lands in the already-open log.
useEffect(() => {
const onFault = (e: Event) => {
const detail = (e as CustomEvent).detail as { message?: string } | undefined;
if (detail?.message) setMessage({ type: 'error', text: detail.message });
if (!detail?.message) return;
const text = detail.message;
setCompileLogs((prev) => [
...prev,
{ timestamp: new Date(), type: 'error', message: text, target: CIRCUIT_CHECK_TARGET },
]);
};
window.addEventListener('velxio-circuit-fault', onFault);
return () => window.removeEventListener('velxio-circuit-fault', onFault);
}, []);
}, [setCompileLogs]);
useEffect(() => {
if (!moreMenuOpen) return;
@ -660,36 +683,52 @@ export const EditorToolbar = ({
}, []);
/**
* Returns true if the caller should proceed inline. If the verifier finds
* errors we stash a resume callback in `pendingRunRef` and pop the
* verification modal; the resume callback re-enters `handleRun` with
* `skipVerify = true` so we don't loop. Warnings-only results don't
* block they surface inline via `setMessage` and the run continues.
* Returns true if the caller should proceed inline. All findings are written
* to the output console (red errors / orange warnings, "Circuit check"
* group). If the verifier finds errors we also stash a resume callback in
* `pendingRunRef` and pop the verification modal; the resume callback
* re-enters `handleRun` with `skipVerify = true` so we don't loop.
* Warnings-only results don't block the console entry is enough.
*/
const checkOrBlock = useCallback(
async (resume: () => void): Promise<boolean> => {
const result = await runVerification();
if (!result) return true;
if (result.errors.length === 0 && result.warnings.length === 0) return true;
if (result.errors.length === 0) {
// Warnings only — non-blocking. Surface inline and continue.
const summary = result.warnings
.slice(0, 3)
.map((w) => w.message)
.join(' • ');
const more = result.warnings.length > 3 ? ` (+${result.warnings.length - 3} more)` : '';
setMessage({
type: 'error',
text: `${result.warnings.length} circuit warning${result.warnings.length === 1 ? '' : 's'}: ${summary}${more}`,
});
return true;
}
// Errors → block until the user explicitly chooses Run Anyway.
// Write every finding to the output console under "Circuit check" — red
// for errors, orange for warnings — so there's one persistent, unified
// diagnostics log next to the compiler output (Proteus-style). Replace
// any prior circuit-check entries so repeated runs stay clean, and open
// the console so the findings are visible.
const now = new Date();
setCompileLogs((prev) => [
...prev.filter((l) => l.target?.id !== CIRCUIT_CHECK_TARGET.id),
...result.errors.map((e) => ({
timestamp: now,
type: 'error' as const,
message: e.message,
target: CIRCUIT_CHECK_TARGET,
})),
...result.warnings.map((w) => ({
timestamp: now,
type: 'warning' as const,
message: w.message,
target: CIRCUIT_CHECK_TARGET,
})),
]);
setConsoleOpen(true);
// Warnings only — non-blocking; the console entry is enough, run continues.
if (result.errors.length === 0) return true;
// Errors → also pop the modal so the user makes an explicit Run-anyway /
// Cancel decision; the console keeps the persistent red record.
pendingRunRef.current = resume;
setVerification(result);
return false;
},
[runVerification],
[runVerification, setCompileLogs, setConsoleOpen],
);
const handleRun = async (skipVerify = false) => {
@ -715,7 +754,9 @@ export const EditorToolbar = ({
if (customChips.length > 0) {
setCompiling(true);
setConsoleOpen(true);
setCompileLogs([]);
// Fresh chip output, but keep the circuit pre-flight findings just
// logged by checkOrBlock so they survive a "Run anyway".
setCompileLogs((prev) => prev.filter((l) => l.target?.id === CIRCUIT_CHECK_TARGET.id));
try {
await prepareCustomChips(customChips, files);
} catch (e) {