# Deployment Topology Page — Design

A single page under `/deployment` that owns the Site → Area → Instance hierarchy: structural management (create, rename, move, delete) and instance lifecycle (deploy, enable/disable, configure, diff), built on the existing `TreeView` component with the same V1–V7 visual identity as the templates page.

This page **replaces** both `/deployment/instances` (current read-mostly tree) and `/admin/areas*` (current flat-list CRUD for areas).

## Decisions

| Question | Decision |
|---|---|
| Page identity | Replace both `/deployment/instances` and `/admin/areas*` with one new page |
| Route | `/deployment/topology` |
| Empty containers | Always shown (so they're valid move/create targets) |
| Instance configuration | Stays on dedicated `/deployment/instances/{id}/configure` page |
| Filters | Search-only (single input above the tree) |
| Search semantics | Dim non-matches (50% opacity), preserve tree shape |
| Single-click behavior | Select-only; nothing navigates |
| Rename UX | Inline (F2 / double-click) for areas only. Instance rename is out of scope (see "Instance rename" below). |
| Site-node menu | Add Area, Create Instance here |
| Area-node menu | Add Sub-area, Create Instance here, Move to Area…, Rename…, Delete |
| Instance-node menu | Deploy/Redeploy, Enable/Disable, Configure, Diff, Move to Area…, Delete |
| Delete-area cascade | Keep server semantics — block on any non-empty subtree |
| Top-of-page buttons | Create Area, Create Instance, Refresh |
| Move structural scope | Same-site only (instance↔area, area↔area). Cross-site moves out of scope. |
| Backend area re-parenting | New `AreaService.MoveAreaAsync(int areaId, int? newParentAreaId, string user)` |
| State persistence | Expanded nodes + selected key, both in sessionStorage |
| Glyphs | Site `bi-building`, Area `bi-diagram-3`, Instance `bi-box` |

## Layout

```
┌─────────────────────────────────────────────────────────────┐
│ Topology                                                     │
├─────────────────────────────────────────────────────────────┤
│ [Search box ............................. ]                 │
│ [Create Area] [Create Instance] [Refresh]                   │
├─────────────────────────────────────────────────────────────┤
│ ▾ 🏢 Plant-A                                                 │
│   ▾ ▦ Line-1                                                │
│     ▸ ▦ Station-3                                           │
│     □ Pump-001          [Enabled] [Current]                 │
│     □ Pump-002          [Disabled]                          │
│   ▾ ▦ Line-2                                                │
│     □ Conveyor-01       [NotDeployed]                       │
│ ▾ 🏢 Plant-B                                                 │
│   ▸ ▦ (empty area, still shown)                             │
└─────────────────────────────────────────────────────────────┘
```

## Visual identity

Follows the existing `Component-TreeView.md` V1–V7 guide. Glyphs adopted:

| Node | Glyph | Color hook |
|---|---|---|
| Site | `bi-building` | default |
| Area | `bi-diagram-3` | default |
| Instance | `bi-box` | default; state badge to the right |

Instance state badges (kept from current page):

| State | Badge |
|---|---|
| Enabled | `bg-success` |
| Disabled | `bg-secondary` |
| NotDeployed | `bg-light text-dark` |
| Stale (deployed but template revision drifted) | `bg-warning text-dark` |
| Current | `bg-light text-dark` |

Search dimming: non-matches receive `opacity: 0.4`. Matches keep full opacity. Tree shape is preserved; ancestors of matches are auto-expanded on first keystroke.

## Context menus

### Site
- **Add Area** → opens "Create Area" dialog with this site pre-selected (parent = root)
- **Create Instance here** → navigates `/deployment/instances/create?siteId={id}`

### Area
- **Add Sub-area** → "Create Area" dialog with this area pre-selected as parent
- **Create Instance here** → navigates `/deployment/instances/create?siteId={siteId}&areaId={id}`
- **Move to Area…** → opens `MoveAreaDialog`. Destination list = areas in the same site, excluding self and descendants. Plus "(root of site)" option.
- divider
- **Rename…** → opens `RenameAreaDialog` (also reachable via F2 / double-click for inline edit)
- **Delete** → calls `DeleteAreaAsync`; server rejects if non-empty, error surfaced via toast

### Instance
- **Deploy** / **Redeploy** (label depends on `IsStale`)
- **Enable** / **Disable** (state-dependent)
- **Configure** → navigates `/deployment/instances/{id}/configure`
- **Diff** → opens the existing diff modal (ported from current Instances page)
- **Move to Area…** → opens `MoveInstanceDialog`. Destination list = areas in the same site + "(no area, site root)".
- divider
- **Delete**

## Inline rename

Applies to **Area rows only**. Instance rows do not support rename on this page (see "Instance rename" below).

- `F2` or double-click on the label of an Area row replaces the label span with an `<input>` bound to a local edit buffer.
- `Enter` commits via `AreaService.UpdateAreaAsync(areaId, name, user)`.
- `Escape` cancels.
- On commit failure (e.g., name collision at the same level), the toast shows the server error and the input stays open with the bad value highlighted.

## Instance rename

**Out of scope for this page.** `InstanceService` currently has no rename method. Adding one is non-trivial:

- `Instance.UniqueName` is also the identity of the site-side `InstanceActor` (Akka actor name).
- It appears in deployment records, audit history, and deploy paths.
- Renaming a deployed instance would require coordinated site-side actor stop/restart, deployment-record rebinding, and potentially redeployment.

This warrants its own design pass. For now: an instance row's label is read-only on the topology page. If a rename is needed, the user can delete + recreate (with the limitation that deployment history is lost).

The Area-rename context-menu item ("Rename…") is **not** added to the instance menu.

## Backend changes

### `AreaService.MoveAreaAsync(int areaId, int? newParentAreaId, string user)` — NEW

Parallel to `InstanceService.AssignToAreaAsync`. Validates:

1. Area exists.
2. `newParentAreaId` is null OR refers to an area in the **same site** as the area being moved.
3. `newParentAreaId != areaId` (not self).
4. The new parent is not a descendant of the area being moved (cycle prevention) — reuse the existing descendant-walking helper that `DeleteAreaAsync` uses.
5. No sibling area at the new level has the same name (case-insensitive).

On success: updates `ParentAreaId`, persists, audits as `"Move"` on entity `"Area"`.

`UpdateAreaAsync` stays name-only.

### `Templates.razor` parent-immutability pattern is **not** repeated here
Areas can be moved freely (subject to validation). Templates are different because re-parenting changes inheritance semantics; areas are pure organizational containers.

### No change to:
- `InstanceService.AssignToAreaAsync` (already supports re-parenting; will be called by `MoveInstanceDialog`)
- `AreaService.DeleteAreaAsync` (keep current block-on-non-empty semantics)
- `AreaService.UpdateAreaAsync` (stays name-only)
- `InstanceService` lifecycle methods (already used by current Instances page)

### CLI / ManagementService parity (optional follow-up) — **DONE 2026-08-01**
- ~~Add `MoveAreaCommand` message + `ManagementService` handler that wraps `MoveAreaAsync`.~~ Shipped: `MoveAreaCommand(int AreaId, int? NewParentAreaId)` in `Commons/Messages/Management/SiteCommands.cs`, dispatched by `ManagementActor.HandleMoveArea` (delegates to `AreaService.MoveAreaAsync`; failures surface as the standard curated failure response), gated any-of `[Designer, Deployer]` like the other area mutations.
- ~~Add CLI: `cli area move --id X --parent-id Y …` (omit `--parent-id` to move to site root).~~ Shipped as **`scadabridge site area move --id X [--parent-id Y]`** — the area verbs live under the existing `site area` group, not at the CLI root, so the verb was placed alongside `site area create|update|delete` rather than introducing a second top-level spelling.

Not strictly required to ship the UI page, but worth doing for parity with how the rest of the app exposes admin ops.

## Routes affected

| Route | Before | After |
|---|---|---|
| `/deployment/topology` | — | **NEW** (this page — canonical route) |
| `/deployment/instances` | tree + lifecycle page | **secondary `@page` directive on `Topology.razor`** — old bookmarks continue to work. NavMenu and all internal back-navs retarget to `/deployment/topology`. |
| `/admin/areas` | flat list | **removed** |
| `/admin/areas/add` | dialog page | **removed** (Create Area dialog lives on topology page) |
| `/admin/areas/edit/{id}` | edit page | **removed** (rename via inline / context menu) |
| `/admin/areas/delete/{id}` | confirm page | **removed** (confirm via shared `ConfirmDialog`) |
| `/deployment/instances/create` | unchanged | accepts new `?siteId=` and `?areaId=` query params for preselection |
| `/deployment/instances/{id}/configure` | unchanged | unchanged |

The admin nav entry for "Areas" gets removed; "Topology" goes under the Deployment nav group.

## Files to add

```
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Topology.razor          (~500 lines)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/MoveInstanceDialog.razor (~50 lines)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/MoveAreaDialog.razor     (~55 lines)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/CreateAreaDialog.razor   (~60 lines)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/RenameAreaDialog.razor   (~45 lines)  (optional if inline-only)
```

## Files to modify

```
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/AreaService.cs         (+ MoveAreaAsync, ~40 lines)
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/...                         (interface for AreaService if exposed)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceCreate.razor
                                                              (+ SiteId, AreaId query-param SupplyParameterFromQuery;
                                                               retarget back-nav to /deployment/topology — 3 sites)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor
                                                              (retarget back-nav to /deployment/topology — 1 site)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor      (replace 'Instances' nav with 'Topology' at /deployment/topology;
                                                               remove 'Areas' nav under Admin)
tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/NavigationTests.cs
                                                              (update InlineData: 'Instances' → 'Topology', '/deployment/instances' → '/deployment/topology')
docs/requirements/Component-TreeView.md                       (rewrite §1 'Instances Page' → 'Topology Page' with new route;
                                                               remove §3 'Areas Page')
```

Note: `CLAUDE.md` does **not** reference `/deployment/instances` today, so no edit required there.

## Files to remove

```
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Instances.razor   (replaced by Topology.razor; old route preserved as secondary @page)
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/Areas.razor
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/AreaAdd.razor
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/AreaEdit.razor
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/AreaDelete.razor
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/InstancesPageTests.cs (if it exists)
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/AreaPageTests.cs       (if it exists)
```

Verified there are no other references to `/admin/areas*` in CLI, ManagementService, requirement docs (other than `Component-TreeView.md` §3, which is updated above), or tests.

## State persistence

- `topology-tree` (sessionStorage) — expansion state (Set of node keys), already supported by `TreeView.StorageKey`.
- `topology-tree-selected` (sessionStorage) — selected node key. New; the `TreeView` already exposes `SelectedKey` two-way binding, but the page is responsible for persisting it. Pattern: write in `SelectedKeyChanged`, read on `OnAfterRenderAsync` after data load.

## Tests

### Unit (`tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/AreaServiceTests.cs`)
- `MoveArea_ToOtherArea_Succeeds`
- `MoveArea_ToSiteRoot_Succeeds` (newParentAreaId = null)
- `MoveArea_ToSelf_Fails`
- `MoveArea_ToDescendant_FailsWithCycleError`
- `MoveArea_DifferentSite_Fails`
- `MoveArea_NameCollidesAtNewParent_Fails`
- `MoveArea_NameUniqueAtNewParent_Succeeds`
- `MoveArea_AuditLogged`

### bUnit (`tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TopologyPageTests.cs`)
- `Renders_EmptyState_WhenNoSites`
- `Renders_EmptySite_WhenSiteHasNoAreasOrInstances` (empty containers visible)
- `Renders_SiteAreaInstance_Nesting`
- `Search_DimsNonMatches_PreservesShape`
- `F2_OnAreaRow_EntersRenameMode`
- `F2_OnInstanceRow_DoesNothing` (rename out of scope)
- `EscapeDuringInlineRename_Cancels`
- `ContextMenu_AreaMove_OpensDialogWithCycleFreeOptions`
- `ContextMenu_InstanceMove_OpensDialogWithSameSiteAreasOnly`
- `ContextMenu_SiteCreateInstance_NavigatesWithSiteIdQuery`
- `LegacyInstancesRoute_RoutesToTopologyPage` (visiting `/deployment/instances` resolves to the same component)

### Removal cleanup
- Drop `InstancesPageTests` and any `AreaPageTests` along with the source files.

## Edge cases

- **Two sites with the same area name at root** — fine. Same-site uniqueness is the rule; areas in different sites are independent.
- **Move an area while it has an instance assigned at its root** — allowed. The instance keeps the same `AreaId`; the area's new parent doesn't affect it.
- **Site with no areas, just root instances** — instance rows render directly under the site node.
- **Concurrent rename of a node by another user** — last-write-wins (consistent with template policy).
- **Search match inside a collapsed branch** — auto-expand the ancestor chain so the highlighted match is visible.
- **Network failure during inline rename** — leave the input open with the pending value; show the error in a toast; user can retry or Escape.
- **Deleting an area, then immediately Ctrl+Z** — not supported (no undo); destructive actions are confirmed via `ConfirmDialog` and audited.

## Out of scope

- Cross-site moves (would need new `Instance.SiteId` rebinding semantics, deployment-record handling, name-collision check at new site).
- Drag-and-drop reordering of areas (no ordinal column today; arbitrary alpha-sort).
- Bulk operations (select multiple instances and move/deploy together).
- Search across templates / sites / instances from the same input (the search is scoped to this page's tree).
- **Instance rename.** No `RenameInstanceAsync` in `InstanceService` today; adding one requires a separate design pass (site-side actor identity, deployment-record rebinding, audit history continuity). Users wanting to rename should delete + recreate.

## Out-of-band consistency tasks

When this lands, the following docs need a touch-up:

- `README.md` — component table; verify no reference to the removed Instances/Areas pages remains.
- `docs/requirements/Component-CentralUI.md` (or the routing section if one exists) — route table.
- `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` — if existing CLI examples reference `area` subcommands, align with the optional CLI `area move` addition.

Confirmed clean (no edit needed):
- `CLAUDE.md` does not reference `/deployment/instances` or `/admin/areas` today.
