Two new Blazor components surface every Modbus knob added by #136-#144 so
users can configure the driver without hand-editing DriverConfig JSON.
ModbusAddressEditor.razor (live address-string parser preview):
- Bound to a string AddressString + a Family / MelsecSubFamily hint.
- On every input keystroke, runs ModbusAddressParser.TryParse and surfaces
the resolved breakdown (Region, Offset, DataType, Bit, ByteOrder,
ArrayCount, StringLength) inline as a green badge.
- On parse error, shows the parser's diagnostic in red.
- Re-uses the SAME parser the wire driver uses — grammar drift is
impossible by construction.
ModbusOptionsEditor.razor (driver-instance options panel):
- Connection group (Host / Port / UnitId).
- Family group (#144) with conditional MelsecSubFamily dropdown.
- Keep-alive group (#139): Enabled / Time / Interval / RetryCount.
- Reconnect group (#139): InitialDelay / MaxDelay / BackoffMultiplier.
- Protocol group (#140): MaxRegistersPerRead / Write / Coils / ReadGap.
- Behaviour toggles (#140 + #141): UseFC15 / UseFC16 / WriteOnChangeOnly.
- Bound to ModbusOptionsViewModel — defaults match ModbusDriverOptions
defaults so unedited rows produce the historical wire output verbatim.
Architecture:
- Admin project gains a ProjectReference to Driver.Modbus.Addressing
(the shared parser assembly extracted in #136). Admin does NOT take a
dep on Driver.Modbus itself — the addressing concerns are cleanly
separated from the wire driver.
- Same-namespace shared assembly means components reference
ModbusAddressParser / ModbusFamily / etc. without prefix gymnastics.
Tests:
- ModbusOptionsViewModelTests (1 test) — pins every default in the view
model against the corresponding ModbusDriverOptions default. A
regression that flips an unedited row to a non-default value gets
caught here. (Test references both Admin and Driver.Modbus to make the
cross-assembly comparison.)
- Live Blazor component testing requires bUnit, which isn't currently
in the test setup; the parser logic the component wraps is fully
covered by the 91 ModbusAddressParser tests in the addressing project,
so the glue layer's behaviour is verifiable end-to-end already.
Caveat: the wiring into the existing DriverInstance edit page lives in
DriversTab.razor — that integration is left as a follow-up because it
touches the cluster-edit workflow specifically and the components in
this commit are framework-agnostic enough to drop in. The components
build clean against the existing Admin project; no behavioural change
to other tabs.
Two small fixes so `scripts/compliance/phase-6-all.ps1` exits 0 — this is
GA exit-criterion #1 from docs/v2/v2-release-readiness.md.
1. Admin csproj: bump OpenTelemetry.Extensions.Hosting 1.15.2 → 1.15.3 +
OpenTelemetry.Exporter.Prometheus.AspNetCore 1.15.2-beta.1 →
1.15.3-beta.1. Fixes NU1902 moderate-severity advisory
(GHSA-g94r-2vxg-569j) on the transitive OpenTelemetry.Api 1.15.2 pull.
TreatWarningsAsErrors on the Admin project promoted the advisory to an
error and failed the whole `dotnet test` run at restore.
2. SchemaComplianceTests.All_expected_tables_exist: the expected-tables
list drifted behind four Phase 7 migration additions — Script,
ScriptedAlarm, ScriptedAlarmState, VirtualTag. The EF model + live
migrations have carried these tables for a while; the compliance test
just needed the four names added. Applied migrations against a scratch
DB to confirm the list is exhaustive.
Verification: full solution test pass 2301 / 2301 (one tolerated
pre-existing CLI flake). Phase 6 aggregate compliance: all four phases
PASS with no test-count regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the draft-editor tab + page surface for authoring Phase 7 virtual tags and
scripted alarms, plus the /alarms/historian operator diagnostics page. Monaco loads
from CDN via a progressive-enhancement JS shim — the textarea works immediately so
the page is functional even if the CDN is unreachable.
## New services (Admin)
- ScriptService — CRUD for Script entity. SHA-256 SourceHash recomputed on save so
Core.Scripting's CompiledScriptCache hits on re-publish of unchanged source + misses
when the source actually changes.
- VirtualTagService — CRUD for VirtualTag, with Enabled toggle.
- ScriptedAlarmService — CRUD for ScriptedAlarm + lookup of persistent ScriptedAlarmState
(logical-id-keyed per plan decision #14).
- ScriptTestHarnessService — pre-publish dry-run. Enforces plan decision #22: only
inputs the DependencyExtractor identifies can be supplied. Missing / extra synthetic
inputs surface as dedicated outcomes. Captures SetVirtualTag writes + Serilog events
from the script so the operator can see both the output + the log output before
publishing.
- HistorianDiagnosticsService — surfaces the local-process IAlarmHistorianSink state
on /alarms/historian. Null sink reports Disabled + swallows retry. Live
SqliteStoreAndForwardSink reports real queue depth + last-error + drain state and
routes the Retry-dead-lettered button through.
## New UI
- ScriptsTab.razor (inside DraftEditor tabs) — list + create/edit/delete scripts with
Monaco editor + dependency preview + test-harness run panel showing output + writes
+ log emissions.
- ScriptEditor.razor — reusable Monaco-backed textarea. Loads editor from CDN via
wwwroot/js/monaco-loader.js. Textarea stays authoritative for Blazor binding; Monaco
mirrors into it on every keystroke.
- AlarmsHistorian.razor (/alarms/historian) — queue depth + dead-letter depth + drain
state badge + last-error banner + Retry-dead-lettered button.
- DraftEditor.razor — new "Scripts" tab.
## DI wiring
All five services registered in Program.cs. Null historian sink bound at Admin
composition time (real SqliteStoreAndForwardSink lives in the Server process).
## Tests — 13/13
Phase7ServicesTests covers:
- ScriptService: Add generates logical id + hash, Update recomputes hash on source
change, Update same-source keeps hash (cache-hit preservation), Delete is idempotent
- VirtualTagService: round-trips trigger flags, Enabled toggle works
- ScriptedAlarmService: HistorizeToAveva defaults true per plan decision #15
- ScriptTestHarness: successful run captures output + writes, rejects missing /
extra inputs, rejects non-literal paths, compile errors surface as Threw
- HistorianDiagnosticsService: null sink reports Disabled + retry returns 0