When any equipment's REM signal switches to local mode, fire a dedicated
`unit.rem_local` event (with unit + equipment context) and record it to
the event log. Also fire `unit.rem_recovered` when all REM signals return
to remote. AutoControlStopped is still fired alongside RemLocal when
auto was running at the time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `rem_local: bool` to UnitRuntime; set true when any equipment's
REM signal is false with good quality
- Engine check_fault_comm: stop auto-control and fire AutoControlStopped
when any equipment switches to local mode
- Block start-auto when rem_local (backend + error message)
- Frontend: disable Start Auto button in units/ops views when rem_local
- Frontend: disable equipment Start/Stop buttons in config view when
unit's rem_local is true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents accidentally binding two points with the same role to the
same equipment at the database level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reuses the existing API.md drawer for README; switching between
docs reloads content and updates the drawer title. Backend serves
README.md via /api/docs/readme-md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loadPoints() relies on state.equipmentMap to display bound equipment names.
Running it in parallel with loadEquipments() caused a race condition where
points rendered before the map was populated, showing all as "Unbound".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- start-auto now requires fault_locked=false and manual_ack_required=false
- batch-start-auto now also skips units with manual_ack_required
- add manual_ack_required to pre-flight check list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signal indicators are now wider pill badges (40×20px rect) with the role
label (REM/RUN/FLT) embedded inside, replacing the 10px dot+label rows.
Equipment Start/Stop buttons are disabled when:
- auto control is active
- REM = 0 (device in local mode, not accepting remote commands)
- FLT = 1 (fault active)
Button state reacts in real time to WS signal updates via a per-equipment
syncBtns closure registered in state.opsUnitSyncFns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevent starting unit auto control while fault_locked or manual_ack_required,
enforcing that faults must be manually acknowledged before resuming automation.
Also disable the Start Auto button in the frontend with descriptive tooltips.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Events were only loaded when switching to config view, but the
system events panel lives in ops view, leaving it empty on refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ops view now reads equipment+role_points from state.units (returned by
unit list API) instead of state.equipments, eliminating the loadEquipments()
call on bootstrap.
Config data (sources, equipments, events, points) is deferred until the
user first switches to config view. On WS reconnect, loadEquipments is
only refreshed when config view is active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit list and single-unit endpoints now include per-unit equipment list
with signal-role points and monitor data, consistent with unit detail.
Uses batch queries to avoid N+1 DB calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moved simulate_run_feedback from command.rs into simulate.rs where it
reuses patch_signal. command.rs now only contains real PLC command logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When SIMULATE_PLC=true, a background task randomly disrupts rem or flt
signals on equipment (rem=false for 5-15s, flt=true for 3-10s) to
exercise fault detection, comm lock, and recovery logic in the engine.
Uses XorShift64 PRNG with no extra dependencies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Event messages are now stored and displayed in Chinese. Names/codes are
resolved via lightweight DB lookups in persist_event_if_needed (entities
still exist at processing time). SourceDelete passes the name explicitly
since the source is deleted before the async event is processed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After a disconnect/reconnect, re-fetch units (runtimes) and equipments
(monitor data) so the UI reflects current server state without requiring
a page reload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Equipment list response now includes signal-role points with monitor data,
so the ops view can render signal dots directly from state.equipments
without fetching /api/unit/:id/detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
startOps() was calling loadAllEquipmentCards() redundantly — the
units-loaded event listener already calls it after loadUnits()
completes. This caused two parallel requests to /api/unit/:id/detail
for every unit on page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the standalone GET /api/unit/runtimes endpoint in favour of
embedding runtime directly in existing responses:
- GET /api/unit → each item now includes `runtime` field
- GET /api/unit/:id → returns UnitWithRuntime
- GET /api/unit/:id/detail → UnitDetail now includes `runtime`
runtime is null when the engine has not yet initialised the unit.
Frontend loadUnits() reads the embedded runtime field to populate
state.runtimes — one request instead of two.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: state.runtimes was empty after refresh because the engine
only pushes UnitRuntimeChanged on state transitions — if the engine
is mid-wait-phase, no push occurs and badges show OFFLINE.
Fix: add GET /api/unit/runtimes batch endpoint (returns all known
runtimes as { unit_id: UnitRuntime }) and call it in parallel with
the unit list fetch inside loadUnits(), so runtime badges are correct
immediately after page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- engine.rs: replace HashSet<Uuid> with HashMap<Uuid, JoinHandle> in
supervise(); use is_finished() to detect exited tasks so units that
are disabled then re-enabled get a new task on next 10s scan
- control/mod.rs: extract shared monitor_value_as_bool (using the more
complete validator version that includes "yes"); remove duplicate
copies from engine.rs and validator.rs
- runtime.rs: fix get_or_create_notify TOCTOU by using entry API
instead of read-drop-write pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ops.js: remove unused `formatValue` import
- logs.js: remove `export` from internal-only `appendLog`
- state.js: fix stale comment ({ valueEl, qualityEl } → { dotEl })
- docs: rewrite both plan docs in Chinese; update dual-view-web plan to
reflect actual implementation (sigDotClass dots, loadAllEquipmentCards,
syncEquipmentButtonsForUnit, batch auto buttons in startOps)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>