/** * The what-if panel: the controls that make this a twin rather than a dashboard. * * Setpoints are applied on release rather than on every slider tick, so dragging * does not spam the model with intermediate values and pollute the event log. * * Controls the telemetry source cannot honour are disabled with a reason. A read * -only historian or MQTT feed lets you observe the plant, not drive it, and the * UI must say so rather than offering buttons that silently do nothing. */ import { useEffect, useState } from 'react'; import { useStore } from '../store.js'; import { control } from '../hooks/useTelemetry.js'; export function WhatIfPanel() { const frame = useStore((s) => s.frame); const meta = useStore((s) => s.meta); const [setpoint, setSetpoint] = useState(305); const [lineSpeed, setLineSpeed] = useState(100); const [error, setError] = useState(null); const serverSetpoint = frame ? frame.controls.setpoint : 305; const serverLineSpeed = frame ? frame.controls.lineSpeedPct : 100; // Follow the server unless the user is mid-drag, so a reset or an external // change is reflected here instead of the slider drifting out of sync. const [dragging, setDragging] = useState(null); useEffect(() => { if (dragging !== 'setpoint') setSetpoint(serverSetpoint); }, [serverSetpoint, dragging]); useEffect(() => { if (dragging !== 'lineSpeed') setLineSpeed(serverLineSpeed); }, [serverLineSpeed, dragging]); if (!frame || !meta) return null; const caps = meta.source.capabilities; const activeFaults = new Set(frame.faults.map((f) => f.id)); const send = async (path, body) => { try { setError(null); await control(path, body); } catch (err) { setError(err.message); } }; return (

What-if controls {caps.setpointControl ? 'live on the model' : 'read-only source'}

{!caps.setpointControl && (
This telemetry source is read-only, so setpoints and faults cannot be driven from here.
)} {error &&
{error}
}
Oven setpoint {setpoint} °C
{ setDragging('setpoint'); setSetpoint(Number(e.target.value)); }} onMouseUp={(e) => { setDragging(null); send('setpoint', { value: Number(e.target.value) }); }} onTouchEnd={(e) => { setDragging(null); send('setpoint', { value: Number(e.target.value) }); }} onKeyUp={(e) => { setDragging(null); send('setpoint', { value: Number(e.target.value) }); }} /> Zone 2 responds with a ~50 s lag. Reject rate climbs once deviation passes about 6 °C.
Line speed {lineSpeed}%
{ setDragging('lineSpeed'); setLineSpeed(Number(e.target.value)); }} onMouseUp={(e) => { setDragging(null); send('line-speed', { value: Number(e.target.value) }); }} onTouchEnd={(e) => { setDragging(null); send('line-speed', { value: Number(e.target.value) }); }} onKeyUp={(e) => { setDragging(null); send('line-speed', { value: Number(e.target.value) }); }} /> CNC-02 is the bottleneck at 4.4 s. Faster cycles raise spindle load.

Inject a fault

{meta.faults.map((f) => { const on = activeFaults.has(f.id); return (
{f.label} {f.station} · {f.severity}
); })}
Resets CNC-02 tool wear, as a maintenance intervention would.
); }