/** * 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 (