# Equipment Detail Page (UNS) — Design **Date:** 2026-06-11 **Status:** Approved (brainstorming) — ready for implementation plan **Author:** design session ## Goal Replace the modal-based equipment editor on the global UNS page with a dedicated, tabbed **Equipment detail page**. The page owns the equipment's identity form plus its child collections — **Tags**, **Virtual Tags**, and **Alarms** — each on its own tab. The `/uns` tree is simplified so that Equipment is a leaf that links to this page; tags and virtual tags no longer appear inline in the tree, and the standalone scripted-alarm pages are removed. ## Motivation Today `GlobalUns.razor` (`/uns`) is a single page juggling seven modals (Area / Line / Equipment / Tag / VirtualTag / Import / delete-confirm) and a lazy-expanding tree whose Equipment nodes load Tags + VirtualTags as leaf children. Scripted-alarm *definitions* live on a separate pair of pages (`/scripted-alarms`, `/scripted-alarms/{id}`) that the UNS tree never links to, so an equipment's alarms are managed in a different place from its tags. This spreads one equipment's configuration across a deep tree and three unrelated surfaces. Consolidating everything for a single equipment onto one tabbed page makes the per-equipment workflow coherent and trims the `/uns` tree back to the pure hierarchy (Area → Line → Equipment). ## Decisions (settled in brainstorming) 1. **Tab set:** `Details · Tags · Virtual Tags · Alarms`. *Details* is the first tab and hosts the equipment identity form + save. 2. **"Scripts" = Virtual Tags with their inline bound script source** — i.e. today's `VirtualTagModal` behaviour (the inline Monaco editor) surfaced under the *Virtual Tags* tab. 3. **Route:** `/uns/equipment/{equipmentId}` (nested under UNS), plus `/uns/equipment/new` for create. 4. **Alarms:** the standalone global pages (`/scripted-alarms` list + `/scripted-alarms/{id}` editor) are **removed**; scripted-alarm definitions are managed only from the equipment page's *Alarms* tab. Tradeoff accepted: no fleet-wide alarm list — alarms are reached per equipment. 5. **In-page tabs, single route** (not route-per-tab): one page, a thin `nav-tabs` strip flips `_activeTab`, content switches via `@if`. No state loss on tab switch; no reusable `` component is introduced (YAGNI). 6. **Maximum reuse of the risky UI:** the existing `TagModal` (driver-typed `TagConfigEditorMap` editors) and `VirtualTagModal` (inline Monaco script editor) are reused **unchanged** as the add/edit dialogs on their tabs — they already accept a fixed `EquipmentId`. Only the *Alarms* editor is genuinely new. ## Approach (chosen) vs alternatives - **A — Single page, in-page tabs, reuse existing modals (CHOSEN).** Faithful to "tabs on the new page"; reuses the two hardest pieces (driver-typed tag editors, Monaco) with zero rework; keeps everything testable through `IUnsTreeService`; one route, no tab-switch state loss. - **B — Route-per-tab (the `ClusterNav` precedent).** `/uns/equipment/{id}`, `/…/tags`, `/…/virtual-tags`, `/…/alarms` as separate routed pages sharing an `EquipmentNav`. Rejected: every tab switch is a full navigation (unsaved Details edits lost), create flow is awkward before an id exists, and more files for no functional gain. - **C — Keep modals, add a thin detail page.** Rejected: doesn't satisfy moving tags/scripts/alarms into tabs. ## Architecture ### New page — `Components/Pages/Uns/EquipmentPage.razor` ```razor @page "/uns/equipment/new" @page "/uns/equipment/{EquipmentId}" @attribute [Authorize] // mirror GlobalUns @rendermode RenderMode.InteractiveServer @inject IUnsTreeService Svc @inject NavigationManager Nav ``` - `[Parameter] public string? EquipmentId`; `IsNew => string.IsNullOrEmpty(EquipmentId)`. - `[SupplyParameterFromQuery] public string? LineId` — carries the parent line on create (set by the tree's "Add equipment"). - `OnParametersSetAsync` loads the `EquipmentEditDto` (when editing), the line options, the driver options, then the active tab's list. - **Tab mechanism:** plain `_activeTab` field (default `"details"`); a Bootstrap `nav-tabs` strip flips it; content is `@if (_activeTab == "...")`. While `IsNew`, the Tags / Virtual Tags / Alarms tabs render a "Save the equipment first" placeholder (no id to scope children to yet). ### Create → edit flow (ScriptEdit `/new` → `/{id}` precedent) - On *create*, the Details tab's `SaveAsync` calls `CreateEquipmentAsync(input)` then `Nav.NavigateTo($"/uns/equipment/{createdId}")`, landing on the edit page with all tabs live. - This requires the system-generated `EQ-…` id back from the create. Add a nullable **`CreatedId`** to `UnsMutationResult` (populated by the create methods); `Ok` / `Error` / concurrency semantics are unchanged. - Edit saves call `UpdateEquipmentAsync(id, input, rowVersion)`; `DbUpdateConcurrencyException` surfaces the existing reload message. ### Details tab The `FormModel` + DataAnnotations + `SaveAsync` are lifted **verbatim** out of `EquipmentModal.razor` (Name regex `^[a-z0-9-]{1,32}$`, MachineCode, UnsLineId select, DriverInstanceId select, ZTag/SAPID, the OPC 40010 identity fields, Enabled). Once this lands, `EquipmentModal.razor` is deleted. ### Tags tab - Table of the equipment's tags (Name, driver, DataType, AccessLevel) with Add / Edit / Delete. - Add/Edit **reuse `TagModal` unchanged** (fixed `EquipmentId`, `Existing TagEditDto`, driver options via `LoadTagDriversForEquipmentAsync`); `OnSaved` reloads the list and closes the modal. - Delete → `DeleteTagAsync(tagId, rowVersion)`. - New service method `LoadTagsForEquipmentAsync(equipmentId)` returns a small row-DTO list with the table columns (the existing `LoadEquipmentChildrenAsync` returns tree `UnsNode`s without driver/type columns). ### Virtual Tags tab - Table of VirtualTags (Name, DataType, bound Script, triggers, Enabled); Add/Edit **reuse `VirtualTagModal` unchanged** (inline Monaco source editor comes along for free); Delete → `DeleteVirtualTagAsync`. - New `LoadVirtualTagsForEquipmentAsync(equipmentId)` for the table; script options via existing `LoadScriptsAsync()`. ### Alarms tab Mirrors the Tags / Virtual Tags shape (table + modal + delete). The standalone alarm pages used EF directly; that logic moves **into `IUnsTreeService`** so the tab is service-tested like the rest: - `LoadAlarmsForEquipmentAsync(equipmentId)` → row DTOs (Name, AlarmType, Severity, predicate script, Enabled, RowVersion). - `LoadScriptedAlarmAsync(scriptedAlarmId)` → full `ScriptedAlarmEditDto`. - `CreateScriptedAlarmAsync(equipmentId, ScriptedAlarmInput)` (returns `CreatedId`), `UpdateScriptedAlarmAsync(id, input, rowVersion)`, `DeleteScriptedAlarmAsync(id, rowVersion)` — same `RowVersion` guard pattern as the rest of the service. - New AdminUI types `ScriptedAlarmInput` (Name, AlarmType, Severity, MessageTemplate, PredicateScriptId, HistorizeToAveva, Retain, Enabled) and `ScriptedAlarmEditDto`. **No equipment picker** — equipment is fixed by the tab (the one field the old page had that we drop). - New `Components/Shared/Uns/ScriptedAlarmModal.razor` (shell modeled on `TagModal`): the alarm fields, a script `