# Architecture Review 04 — AdminUI (Blazor Server) - **Date:** 2026-08-19 - **Commit:** `12b0035a` (master, clean tree) - **Updates:** the 2026-07-12 re-review at `f6eaa267` (which updated the 2026-07-08 original at `9cad9ed0`). **This is the second update, and unlike the first it lands on a heavily-changed surface:** 177 commits touched `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI` since `f6eaa267` — the R2 remediation wave (R2-02/-05/-10), the ACL authoring surface (#520 stage 7), the `/apikeys` + Secrets pages, the whole `/raw` project tree (RawTree + 9 modals + CSV import/export + browse-commit), four new driver editor families (Calculation, Sql, Mqtt, MTConnect), the 2026-08-10 UI sweep, the density pass, and a run of targeted perf commits (drift watermark, projections, snapshot caches). Every prior finding was re-verified against code at `12b0035a`; no remediation claim was carried on trust. - **Scope:** `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI` (111 razor components — up from 77; ScriptAnalysis backend, minimal APIs, 3 SignalR bridges — the fleet-status chain was deleted end-to-end in #542, UNS/Raw/Acl service layer), plus `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests` (978 `[Fact]`/`[Theory]` cases, up from 443). --- ## Prior-finding status (f6eaa267 → 12b0035a) | ID | One-liner | Status @ 12b0035a | |---|---|---| | S-1 (Med) | Three components don't drain in-flight timer callbacks on dispose | **PARTIALLY CLOSED** — `Fleet.razor` fully fixed (`63081d52`/`4b6a93bb`/`445bb855`: `IAsyncDisposable` + page-lifetime CTS + `Interlocked` tick-overlap guard + drain, `Fleet.razor:131–210`; pinned by `PollTimerTeardownOrderingTests`). The other two stragglers unchanged: `AlarmsHistorian.razor:85` is still `public void Dispose() => _timer?.Dispose();` and `DriverTestConnectButton.razor:68–81` still runs `System.Timers.Timer` with an async `Elapsed` lambda + sync dispose. | | S-2 (Med) | `Fleet.LoadAsync` no catch; `AlarmsHistorian` bare `catch { }` leaves "Loading…" forever | **PARTIALLY CLOSED** — Fleet now catches, but only `OperationCanceledException`/`ObjectDisposedException` (`Fleet.razor:167–171`); a transient SQL fault still faults the discarded `InvokeAsync` task silently (state no longer wedges — the `finally` at 172–181 resets the guard — but no error surfaces). `AlarmsHistorian.razor:68–74` still bare-catches with the admitted "lands when we add role gating" TODO. | | S-3 (Med) | No `` anywhere | **STILL OPEN** — `grep -rn ErrorBoundary` over the project returns nothing, now across 111 components. | | S-4 (Med) | Delete flows re-fetch RowVersion at click time, defeating the concurrency guard | **STILL OPEN on the UNS pages, NOT propagated to new code** — `GlobalUns.razor:280–311` ("Loads the entity's RowVersion first") and `EquipmentPage.razor:449–452, 494–497` ("Load … fresh to capture its current RowVersion") unchanged. But every NEW delete carries the *rendered* RowVersion: `RawTree.razor:462/469/498/546/640/659`, `AclEdit.razor:264/288`, and the reference remove `EquipmentPage.razor:407`. The codebase now demonstrates the correct pattern beside the wrong one. | | S-5 (Med) | No confirmation on destructive actions | **STILL OPEN — surface grew.** Unchanged: `EquipmentPage.razor:242/297` (vtag/alarm delete), `ScriptEdit.razor:70→142`. New unconfirmed instances: `ApiKeys.razor:102–107` (Revoke + **Delete** of a live data-plane credential, single click), `AclEdit.razor:150` (grant delete), `EquipmentPage.razor:189` (reference Remove). Counter-example now in-tree: `RawTree` routes every delete through `RawConfirmDialog` (`RawTree.razor:466–470` etc.). | | S-6 (Low) | `DriverStatusPanel` result-chip timer lacks the stale-fire token guard `Alerts` has | **STILL OPEN** — `Alerts.razor:260–268` has the `ReferenceEquals(_opResultClearToken, token)` guard; `DriverStatusPanel.razor:382–390` clears unconditionally (its comment even says "mirrors DriverStatusPanel" in Alerts — the mirror is one-way). | | S-7 (Low) | `AdminOperationsClient.AskAsync` bypasses the 10 s ask timeout | **STILL OPEN** — `Clients/AdminOperationsClient.cs:57–58` forwards only the caller's token; the three typed methods still double-guard (`:33–54`). | | P-1 (Med) | Monaco pushes the full document to .NET on every keystroke, undebounced | **STILL OPEN** — `wwwroot/js/monaco-init.js:213–214`. | | P-2 (Med) | ScriptAnalysis recompiles from scratch per request | **PARTIALLY CLOSED** — the *catalog* half is fixed: `ScriptTagCatalogSnapshotCache` (`3795899d`, singleton, 5 s TTL, gate-collapsed rebuild — `ScriptAnalysis/ScriptTagCatalogSnapshotCache.cs`) removed the six-table scan per keystroke that tag-path completion used to pay. The Roslyn `(text → tree/compilation)` memoization is still absent (`ScriptAnalysisService.cs:60–65` builds a fresh `CSharpCompilation` per call). | | P-3 (Low) | Per-circuit DB polling on dashboard pages | **STILL OPEN (accepted)** — `Hosts` 5 s, `Fleet` 10 s per circuit; the overlap/teardown hazards around those ticks are now fixed (see S-1). | | P-4 (Low) | `Deployments` runs a full config snapshot + hash per page load for the drift badge | **CLOSED** — `dbc9a132` lists a projection (`Deployments.razor:94–105`, no artifact blobs) and `684e59d8` added `DeploymentDriftProbe` (`Deployments/DeploymentDriftProbe.cs`, singleton at `EndpointRouteBuilderExtensions.cs:112`): recompose only when the `ConfigEdit` `(MAX(EditedAtUtc), COUNT(*))` watermark or the sealed hash moves, gate-collapsed, staleness trade documented. SQL-Server-backed pins in `c149410b`. | | P-5 (Low) | `Alerts` rows lack `@key` while prepending | **STILL OPEN** — `Alerts.razor:281` `_rows.Insert(0, evt)`, row markup carries no `@key` (Hosts/EquipmentPage tables do). | | P-6 (Low) | `inlay-hints` round-trips for a documented no-op | **STILL OPEN** — `monaco-init.js:107` posts; `ScriptAnalysisService.cs:472` returns empty. | | C-1 (High) | Authorization idioms / ungated mutating surface | **CLOSED AND HELD.** R2-05's one-idiom state survived 177 commits: all 30 routable pages carry an explicit named-policy `@attribute` (only `Login` exempt, `[AllowAnonymous]`); the six pages added since were consciously classified — `ApiKeys` = FleetAdmin ("minting a key hands out a data-plane credential"), `SecretsAdmin` = the Secrets RCL's own `ManagePolicy`, `GlobalRaw`/`AclEdit` = ConfigEditor, `ClusterAcls` = AuthenticatedRead (verified read-only: `ClusterAcls.razor:107–108` is the only DB touch, `AsNoTracking`), `Dev/ContextMenuDemo` = AuthenticatedRead. `PageAuthorizationGuardTests` still reflects over `typeof(Routes).Assembly` and its census includes every new page (`Authorization/PageAuthorizationGuardTests.cs:29–79`). `/api/script-analysis/*` = ConfigEditor (`ScriptAnalysisEndpoints.cs:18`). `Certificates` keeps markup + imperative FleetAdmin defense-in-depth (`Certificates.razor:85, 180–186`). The anti-drift guard did its job. | | C-2 (Med) | CLAUDE.md ScriptAnalysis policy drift | **STILL FIXED** — CLAUDE.md now says "gated by the `ConfigEditor` policy (Administrator or Designer)", matching `ScriptAnalysisEndpoints.cs:18` (the policy constant replaced the role string; semantics identical). | | C-3 (Good) | Driver-typed tag-editor pattern complete and consistent | **HOLDS, EXTENDED** — `TagConfigEditorMap.cs:17–27` and `TagConfigValidator.cs:19–29` now cover **11** drivers (added Calculation, Sql, Mqtt, MTConnect) with parallel keys, all keyed off `DriverTypeNames` constants (the casing-drift class is structurally closed; #558 consolidated the browse-commit parity guard onto one list, `17130d9c`). Authoring↔runtime parity got a new backstop: `TagConfigParseGate` makes authoring models consult the driver's own runtime parse (#554/#549/#551). | | C-4 (Good) | Numeric-enum serialization bug class closed | **HOLDS** — every new enum-carrying driver form carries `JsonStringEnumConverter` (`SqlDriverForm.razor:121`, `MTConnectDriverForm.razor:170`, `CalculationDriverForm.razor:52`, …); `MqttDriverForm` routes through the driver's own shared `MqttJson.Options` (`MqttDriverForm.razor:7–8`) — same discipline, different mechanism. Per-driver `*FormSerializationTests` extended. | | C-5 (Med) | Two data-access idioms: service seam vs EF-in-page | **STILL OPEN, GREW** — 15 components now inject `IDbContextFactory` directly (was 5 named): all the prior offenders plus `Reservations`, `ClusterOverview/Redundancy/Edit/Acls/Audit/List`, `NewCluster`, `NodeEdit`, `AclEdit`. Meanwhile the *new* Raw/Acl surfaces went through real seams (`IRawTreeService`, `IAclScopeService`, `IApiKeyAdminService`) — the split is now seam-for-new, EF-for-old. | | C-6 (Low) | Naming/marker nits | **STILL OPEN (reduced)** — `"GalaxyMxGateway"` literal survives in `DriverIdentitySection.razor:38` and `GalaxyAddressPickerBody.razor:136` (though `RawDriverTypeDialog.razor:60` now documents + guards the mapping); dispatch maps moved onto `DriverTypeNames`. | | C-7 (Med) | `ResilienceFormModel` lossy round-trip; malformed JSON silently overwritten | **CLOSED (R2-02)** — `ResilienceFormModel.cs:13–17/43–48/106–124`: `JsonObject` bag preserves unknown top-level keys, unknown capabilities, unknown per-policy fields; a malformed blob sets `ParseFailed`, `ToJson` returns the stored text **verbatim**, and `DriverResilienceSection.razor:9–14` shows the warning + explicit discard button while every input is `disabled="@_m.ParseFailed"` (`:40–42`) and `EmitAsync` early-returns (`:93`). `ResilienceFormModelTests` pins it. | | U-1 | Strong seam tests, zero component rendering coverage | **IMPROVED IN VOLUME, SAME SHAPE** — 978 cases (443 → 978): Acls, Raw (CSV mapper, tree service, modals), Api, Browsing, Grpc, Telemetry, Hosts suites added. Still no bUnit (documented posture) — but a **reflection-driven component-test harness now exists**: `RawBrowseModalStaleOpenTests` drives a real modal class end-to-end (inert `Renderer` subclass for the render handle, reflection for `[Parameter]`s), proving modal `@code` logic *is* testable here without bUnit. | | U-2 | Stub/known-gap behaviors | **STILL OPEN** — InlayHints no-op (`ScriptAnalysisService.cs:472` + active provider `monaco-init.js:107`); `AlarmsHistorian.razor:68–74` role-gating TODO; `MonacoEditor.razor:44` `MarkersChanged` still `object[]`; `GlobalUns.razor:327` "Delete for this node kind is not yet available." | | U-3 | Raw-JSON fallback unvalidated | **SUBSTANTIALLY CLOSED** — typed client validation covers 11 drivers and is now **service-enforced**, not just modal-side: `RawTreeService.cs:977` (create/update), `:1273` (import), `RawTagCsvMapper.cs:228` (CSV rows), `RawTagModal.razor:242` / `RawManualTagEntryModal.razor:243` (modals) — the #535→#559 authoring-vs-deploy-gate arc. Residual: the schemaless textarea for *unmapped* drivers (Galaxy only) still takes any text ("Validated server-side at deploy", `RawTagModal.razor:90`) — the deploy gate is the only check there. | | U-4 | Accessibility gaps | **STILL OPEN** — tree expanders are still text glyphs without `aria-expanded` (`RawTree.razor:176–179`, `UnsTree`), modals have no focus trap/Escape, live tails no `aria-live`. New code at least marks decorative emoji `aria-hidden` (`RawTree.razor:229`). | | U-5 | Resilience runtime status has no AdminUI reader | **CLOSED (R2-10 + #546)** — `DriverStatusPanel.razor:15/77–92/203/263–264/292–303` renders breaker-OPEN / consecutive-failures / in-flight / staleness chips off `IDriverResilienceStatusStore` (in-process, self-HubConnection ban honored), and #546 (`d33e4e20` + `e669fb34`) wired the panel into every `/hosts` driver row via the Controls dialog — with the `Enabled`-in-`@key` recycle trick documented in the markup (`Hosts.razor:283–299`) so a live Enabled flip re-mounts rather than orphaning subscriptions. R2-10's live gate ran (per the R2-10 memory/plans register). | --- ## Maturity Ratings | Dimension | 2026-07-08 | 2026-07-12 | **2026-08-19** | Justification | |---|---|---|---|---| | Stability | 4 | 4 | **4** | The dominant open items moved: Fleet's timer/teardown fully fixed, C-7's silent data loss closed, staleness guards + confirm dialogs are the *norm* in the new Raw surface, and the tick-overlap class got a dedicated test file. Docked at 4 (not higher) because the fixes were applied N−1-wide — five sibling modals still carry the exact superseded-open race that three others were just fixed for (S-8), three EditForms silently no-op on invalid input (S-9), and ErrorBoundary/AlarmsHistorian/DriverTestConnectButton stragglers persist. | | Performance | 3 | 3 | **4** | A deliberate, well-engineered perf sweep landed: drift-watermark probe (P-4 closed), `/deployments` and raw-tree projections that stop shipping blobs, the 5 s tag-catalog snapshot cache, server-side rename script-scan with chunked OR-chains, one guard round-trip per bulk reference add. What remains (Monaco keystroke push, per-request Roslyn compile, per-circuit polling) is bounded at single-admin scale and known. | | Conventions | 3 | 3 | **4** | C-1 closed *and held under drift pressure* — the reflection guard forced conscious classification of all six new pages. C-7 closed to the tag-editor discipline. The editor/validator/parity pattern extended cleanly to four new drivers, keyed off constants. Docked for the EF-in-page split growing (C-5) and the naming residue (C-6). | | Underdeveloped | 3 | 3 | **3.5** | U-5 closed (resilience observability shipped and wired to `/hosts`), U-3 substantially closed (validation now service-enforced), tests 443→978 with a component-harness pattern emerging. Still: no bUnit (by policy) with most new modal orchestration verified only by `/run`, the same four U-2 stubs, and accessibility untouched. | --- ## Architecture Overview (delta since f6eaa267) - **Page map changes:** `+/apikeys` (FleetAdmin), `+/secrets` (Secrets RCL wrapper), `+/raw` (`GlobalRaw` → `RawTree`, the v3 Batch-2 authoring surface), `+/clusters/{id}/acls` list + `AclEdit` + `AclScopePickerModal`/`AclScopeTree` (#520 stage 7), `+Dev/ContextMenuDemo`. **Retired:** the routed `/clusters/{id}/drivers` flow (`DriverTypePicker`, `DriverEditRouter`, 8 `*DriverPage` shells) — form bodies live on inside the `/raw` modals (`DriverConfigModal`/`DeviceModal` + `Forms/`/`DeviceForms/`); the UNS `TagModal` (Batch 3 made equipment Tags reference-only via `AddReferenceModal`). - **Hubs:** 3 (`alerts`, `scriptlog`, `driverstatus`) — `FleetStatusHub` + bridge + broadcaster deleted as a zero-client chain (#542, `f1c18811`). The in-process singleton pattern (DPS bridge → store/broadcaster → component event) unchanged and still honored everywhere. - **New service seams:** `IRawTreeService` (1701 lines, RowVersion-aware, warn-on-rename substring scan — now server-side filtered + chunked, `fe8d132c`/`ca87329a`), `IAclScopeService` (mirrors `DraftValidator.ValidateNodeAclScopes` rule-for-rule so page and deploy gate cannot disagree; blocks save on unresolvable scope, re-checked on submit — `AclEdit.razor:238–242`), `IApiKeyAdminService` (server-side validation, hash-only storage), `RawTagCsvExportReader`, `DeploymentDriftProbe` (singleton), `ScriptTagCatalogSnapshotCache` (singleton). - **UI sweep held:** the scoped-CSS bundle is linked (`App.razor:23`), chip variants documented against the Theme kit (`site.css:20–22`), empty-string columns guarded (`Hosts.razor` Last-error uses `Abbreviate … is { Length: > 0 } … : "—"`), `#504` slicing discipline held — every `[..N]` in the tree is length-guarded or Guid-sourced. --- ## 1. STABILITY ### S-8 (Medium, NEW) — The superseded-open staleness guard was applied to three modals; five siblings share the unguarded shape The 2026-08-05/08 fixes (`87637e4e` RawBrowseModal, `2c19a50b`/`4e3a6e84` RawManualTagEntryModal + RawTagModal) established the class: Blazor does not serialize `OnParametersSetAsync` against its own outstanding task, and a bool open-flag cannot tell two opens apart — so a close-and-reopen onto another target while a load is in flight lets the first open's late continuation install its state under the second open's displayed modal. The fixed modals carry generation counters and post-await re-checks (`RawBrowseModal.razor:362–446`, `RawManualTagEntryModal.razor:183–197`, `RawTagModal.razor:202`), and `RawBrowseModalStaleOpenTests` pins one of them. Five sibling modals have the identical shape with **no** guard: - **`RawCsvImportModal.razor:209–240`** — the sharpest instance. `ResolveTargetAsync` awaits `ResolveGroupContextAsync` and then writes `_effectiveDeviceId` + `_groupPrefix` unconditionally; `_resolved` is a bool, not a generation. A stale resolve retargets *both* the parse (`_groupPrefix` is baked into every row's `EffectiveGroupPath`, `:250`) and the **commit itself** — `Commit()` calls `Svc.ImportTagsAsync(_effectiveDeviceId, rows)` (`:293`), and `ImportTagsAsync` takes a device id with no RowVersion, so unlike the edit modals nothing downstream catches the mistake: the all-or-nothing import lands **in the wrong device**. - **`RawCsvExportModal.razor:88–129`** — same shape, read-only consequence (another device's tags exported under this device's filename). - **`DeviceModal.razor:126–171`** — `_openHandled` bool; `LoadAsync` writes `_name`/`_deviceConfigJson`/`_rowVersion`/`_parentDriverConfig` after two awaits with no re-check. A stale install shows device A's config under device B's title; Save is *accidentally* blocked by the RowVersion mismatch, but Test-connect probes the wrong endpoint with no error. - **`DriverConfigModal.razor:132–165`** — identical to DeviceModal (`_driverConfigJson`/`_resilienceConfig`/`_rowVersion` installed post-await). - **`AddReferenceModal.razor:83–101`** — `_wasVisible` bool; a stale `LoadReferencePickerRootAsync` installs equipment A's picker root under equipment B's open; picks then commit as B's references (`CommitAsync:116` uses the current `EquipmentId`), which the cluster-scope server check will *accept* whenever A and B share a cluster. **Recommendation:** port the `_openGeneration` + post-await `IsStale` idiom (and its reflection-harness test — the pattern is proven) to all five. This is the "ForTest-seam / N−1 dispatch sites" failure mode in modal form: the fix commits named three modals; the class has eight members. ### S-9 (Medium, NEW) — Three EditForms validate with no validation display: invalid input = silent no-op Save `NodeEdit.razor:42` and `AclEdit.razor:36` show the house convention (`DataAnnotationsValidator` + ``); the UNS modals render `ValidationMessage`s. Three pages drifted: - **`ClusterEdit.razor:35–36`** — `DataAnnotationsValidator`, `[Required] Name` (`:195`), no `ValidationSummary`/`ValidationMessage` anywhere in the form. - **`NewCluster.razor:18–19`** — worse: `[Required]` ×3 plus a `[RegularExpression(..., ErrorMessage = "Use uppercase letters, digits, dash, underscore.")]` (`:132–136`) whose message **can never render**. - **`ScriptEdit.razor:32–33`** — `[Required] Name` + `[Required] SourceCode` (`:167–168`), no display. Consequence: clear the Name (or type a lowercase cluster id) and click Save/Create — `OnValidSubmit` never fires, no message appears, the button just does nothing. This is precisely the #505-adjacent "EditForm with no ValidationSummary = silent no-op Save" class the project memory documents, and it passes every unit test because there is no rendering coverage. **Recommendation:** one-line fix per page; add the three `` lines, and consider a guard-style scan (the pages are compiled classes — a reflection test could assert every component that renders `DataAnnotationsValidator` also renders a validation display, mirroring `PageAuthorizationGuardTests`). ### S-1 / S-2 residue (Medium, carried) — Two straggler timers and two swallowed-error paths Fleet is now the model citizen (`Fleet.razor:113–210`: CTS-before-drain, `Interlocked` overlap guard, tick-holding-CTS dispose reasoning — all commented). Still owed: - `AlarmsHistorian.razor:85` sync `Dispose`; `:68–74` bare catch that leaves "Loading…" forever on admin-only nodes (the TODO is now over a year of review cycles old). - `DriverTestConnectButton.razor:68–81` `System.Timers.Timer` + async `Elapsed` (effectively async-void) + sync dispose. - `Fleet.razor:167–171` catches only OCE/ODE — a transient SQL fault is still an unobserved task exception with no error surface (the `Hosts` catch-log-degrade shape was never copied over). ### S-3 (Medium, carried) — No ``; S-4 (Medium, carried) — UNS delete RowVersion refetch; S-5 (Medium, carried) — unconfirmed destructive actions All three persist as described in the table above. Two aggravations worth noting: - **S-4 is now an internal inconsistency, not just a defect**: `RawTree` and `AclEdit` prove the rendered-RowVersion delete works fine in this codebase; `GlobalUns`/`EquipmentPage` sit beside them doing the opposite. - **S-5's sharpest new instance is `ApiKeys.razor:106–107`**: single-click **Delete** of an API key destroys a live machine credential (revocation is immediate on ConfigDb nodes) with no confirm and no undo — and unlike Revoke it also erases the row. At minimum, Delete here deserves the `RawConfirmDialog` treatment; arguably Delete should be gated behind Revoke-first. ### S-6 / S-7 (Low, carried) `DriverStatusPanel.razor:382–390` still lacks the stale-fire token guard its own Alerts twin has (`Alerts.razor:260–268` — whose comment claims to "mirror DriverStatusPanel"); `AdminOperationsClient.cs:57–58` still forwards only the caller's token. ### Positive stability notes - The three fixed modals + `RawBrowseModalStaleOpenTests` show the *harness* problem (no bUnit) is solvable for modal state machines — an inert `Renderer` subclass + reflection drives the real component class. - `RawTree` is a well-built coordinator: one modal set for the whole recursive tree, `@key` per row, lazy loads with captured errors, confirm dialogs on every delete, rendered-RowVersion mutations, warnings surfaced through a shared message modal (`RawTree.razor:705–723`). - `Hosts`' Controls dialog documents its own `@key`-recycle rationale in markup (`Hosts.razor:283–299`) — the "Enabled flips must recycle the panel or its subscriptions dangle" trap is pinned in words where the next editor will read them. - `AlarmsHistorian` aside, every new page reports load errors (`ApiKeys.ReloadAsync` catches and surfaces; `AclEdit` catches `DbUpdateConcurrencyException` with a human message on both save and delete). --- ## 2. PERFORMANCE ### P-1 / P-2 / P-3 / P-5 / P-6 (carried — see table) The two Mediums still worth scheduling: Monaco's undebounced full-document push (`monaco-init.js:213–214`) and the per-request Roslyn compile (`ScriptAnalysisService.cs:60–65` — a size-1 LRU keyed on normalized text would collapse the completions-after-diagnostics case; the catalog half is done). ### Positive performance notes (the sweep that closed P-4) - `DeploymentDriftProbe` is the standout: watermark = `(MAX(EditedAtUtc), COUNT(*))` over the append-only `ConfigEdit` audit (the COUNT half defends same-timestamp edits), volatile-published verdict, gate-collapsed recompose, and an honest "accepted staleness" paragraph naming the exact condition under which the chip lies and why that's tolerable (`Deployments/DeploymentDriftProbe.cs`). - `/deployments` lists a projection (`Deployments.razor:94–105`) instead of 50 artifact blobs; the raw tree renders tag rows without `TagConfig` (`ba977dbe`); the rename script-scan filters server-side and chunks its OR-chain with SQL-Server-backed pins (`fe8d132c`/`ca87329a`/`c149410b`); bulk reference adds pay one guard round-trip (`fa36d64e`); `ScriptTagCatalogSnapshotCache` bounds editor-assist DB load at one six-table scan per 5 s process-wide. - CSV import bounds the upload at 16 MB via `OpenReadStream(maxAllowedSize:)` (`RawCsvImportModal.razor:251`) and stages all-or-nothing review before any write. --- ## 3. CONVENTIONS ### C-1 — CLOSED AND HELD (see table). The reflection guard proved its worth The census in `PageAuthorizationGuardTests` was *maintained*, not bypassed: each of the six post-R2-05 pages appears with a policy and, for the interesting ones, a rationale comment (ApiKeys→FleetAdmin "same authority level as editing role grants"; SecretsAdmin carrying the RCL's own policy). This is the anti-drift outcome R2-05 was designed for. ### C-5 (Medium, carried) — The seam/EF-in-page split is now generational New surfaces (Raw, Acls, ApiKeys, drift, catalog) all went through injectable services with real test suites; the older cluster/script/dashboard pages still run EF in `@code` (15 components inject `IDbContextFactory`). The convention argument has effectively been won by the new code — declare it and migrate the old pages opportunistically (`ScriptEdit`'s save/delete still belongs in the existing script service methods). ### C-6 (Low, carried) — reduced to two `"GalaxyMxGateway"` literals (`DriverIdentitySection.razor:38`, `GalaxyAddressPickerBody.razor:136`); the dispatch-map keying moved to `DriverTypeNames` fleet-wide. ### C-8 (Low, NEW) — A dev demo page ships in the production route table `Components/Pages/Dev/ContextMenuDemo.razor` is routable in every deployment (gated `AuthenticatedRead`, consciously classified in the guard census — so this is a deliberate choice, recorded here as a nit). If it's meant as a pattern gallery, name it so; if not, `#if DEBUG` or delete. ### Positive conventions notes - The tag-editor discipline scaled to 11 drivers without a single map/validator drift, because the keys are constants and the parity tests are consolidated onto one list (#558). - The authoring↔runtime parity arc (#549/#550/#551/#554/#557) gave the AdminUI a genuinely novel convention: authoring models consult the **driver's own runtime parse** (`TagConfigParseGate`) so the editor can never accept a config the runtime rejects — enforced at modal, CSV, and service layers (one authority, service-enforced — the `reference_authoring_vs_deploy_gate_drift` lesson applied). - `AclScopeService` mirrors `DraftValidator.ValidateNodeAclScopes` rule-for-rule with better operator wording, and the picker makes invalid grants *unauthorable* (Cluster ⇒ null ScopeId at projection, `AclScopeService.cs:265–267`; UNS tags deliberately unofferable with the rationale in code, `:317–320`). --- ## 4. UNDERDEVELOPED AREAS ### U-1 (carried, improved) — 978 seam tests, no rendering coverage — but the harness excuse is gone The reflection-driven component harness (`RawBrowseModalStaleOpenTests` — inert `Renderer` subclass for the render handle, reflection past BL0005 for parameters) demonstrates modal `@code` machines are testable without bUnit. S-8's five unguarded modals and S-9's three silent forms are exactly the tests that harness should now grow. ### U-2 (carried) — the same four stubs InlayHints wired-no-op (client provider still POSTs, `monaco-init.js:107`; `ScriptAnalysisService.cs:472`); `AlarmsHistorian` role-gating TODO; `MonacoEditor.MarkersChanged` untyped `object[]`; `GlobalUns` "not yet available" delete branch. None moved in ~1300 commits — either schedule or annotate as accepted. ### U-3 (carried, substantially closed) — residual: the Galaxy schemaless textarea Only unmapped drivers (today: Galaxy) reach the raw textarea, and only the deploy gate validates it. A `JsonDocument.Parse` in the service create/update path for unmapped types remains a cheap backstop. ### U-4 (carried) — accessibility unchanged; the new tree components repeated the text-glyph expander pattern without `aria-expanded` (`RawTree.razor:176–179`, `AclScopeTree`). --- ## Top Recommendations (ordered) 1. **S-8:** Port the superseded-open generation guard to the five unguarded modals — `RawCsvImportModal` first (its stale `_effectiveDeviceId` retargets a commit that nothing downstream can catch). 2. **S-9:** Add the missing `ValidationSummary` to `ClusterEdit`/`NewCluster`/`ScriptEdit`; consider a reflection guard for the class. 3. **S-5 (ApiKeys):** Confirm-dialog the API-key Delete (and AclEdit/EquipmentPage deletes) — a live credential should not die to one click. 4. **S-4:** Carry rendered RowVersions on the UNS deletes (the codebase's own new code shows how) or document delete as last-writer-wins. 5. **S-3:** `ErrorBoundary` around `@Body` — still absent at 111 components. 6. **P-1/P-2:** Debounce Monaco's value push; memoize `(text → compilation)` size-1 in `ScriptAnalysisService`. 7. **S-1/S-2 stragglers:** `AlarmsHistorian` + `DriverTestConnectButton` timers; give `Fleet` (and `AlarmsHistorian`) a real error surface. 8. **C-5:** Declare the service-seam convention won; migrate `ScriptEdit` first.