Compare commits
8 Commits
6d262f7d7c
...
ca21615090
| Author | SHA1 | Date | |
|---|---|---|---|
| ca21615090 | |||
| a474eb6bd6 | |||
| 9e4dedc987 | |||
| 6aa2ee8095 | |||
| e2749b7d69 | |||
| edd49765d6 | |||
| 7e11f9aac8 | |||
| e6e9dbfedb |
@@ -121,7 +121,7 @@ each project's **code-verified current state**, and the **gaps** between. See
|
||||
| Component | Status | Goal | Design | Implementation |
|
||||
|---|---|---|---|---|
|
||||
| Auth (login / identity / authz) | Adopted (lib `0.1.3`; all 3 apps, merged to **local default** main/master + **pushed to origin** (gitea)) | Shared `ZB.MOM.WW.Auth` lib | [`components/auth/`](components/auth/) | [`ZB.MOM.WW.Auth/`](ZB.MOM.WW.Auth/) |
|
||||
| UI Theme (layout / tokens / components) | Built (lib `0.1.0`) | Shared `ZB.MOM.WW.Theme` RCL | [`components/ui-theme/`](components/ui-theme/) | [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) |
|
||||
| UI Theme (layout / tokens / components) | Adopted (lib `0.2.0`; all 3 apps, local `feat/adopt-zb-theme` branches) | Shared `ZB.MOM.WW.Theme` RCL | [`components/ui-theme/`](components/ui-theme/) | [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) |
|
||||
| Health (readiness / liveness / active-node) | Built (lib `0.1.0`) | Shared `ZB.MOM.WW.Health` lib | [`components/health/`](components/health/) | [`ZB.MOM.WW.Health/`](ZB.MOM.WW.Health/) |
|
||||
| Observability (metrics / traces / logs) | Built (lib `0.1.0`) | Shared `ZB.MOM.WW.Telemetry` lib + `.Serilog` | [`components/observability/`](components/observability/) | [`ZB.MOM.WW.Telemetry/`](ZB.MOM.WW.Telemetry/) |
|
||||
| Config + validation (options / startup validation) | Adopted (lib `0.1.0`; all 3 apps, local) | Shared `ZB.MOM.WW.Configuration` lib | [`components/configuration/`](components/configuration/) | [`ZB.MOM.WW.Configuration/`](ZB.MOM.WW.Configuration/) |
|
||||
@@ -156,10 +156,17 @@ backlog. Shared = Technical-Light tokens + IBM Plex fonts + side-rail shell + wi
|
||||
per-project = each app's `site.css` page layout, route content, scoped `.razor.css`.
|
||||
|
||||
The shared RCL is **built and lives in this repo** at [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/)
|
||||
(.NET 10 Razor Class Library; single package; 32 bUnit tests; `dotnet pack` → 1 nupkg @ 0.1.0).
|
||||
The implementation plan is at
|
||||
[`docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md`](docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md).
|
||||
**Not yet adopted** by the three apps — that's the follow-on tracked in [`components/ui-theme/GAPS.md`](components/ui-theme/GAPS.md).
|
||||
(.NET 10 Razor Class Library; single package; 44 bUnit tests; `dotnet pack` → 1 nupkg @ 0.2.0,
|
||||
**published to the Gitea feed**). The build plan is at
|
||||
[`docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md`](docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md);
|
||||
the adoption plan at [`docs/plans/2026-06-03-ui-theme-adoption.md`](docs/plans/2026-06-03-ui-theme-adoption.md).
|
||||
**Adopted across all three apps on 2026-06-03** (full canonical cutover, SPEC §7) on each repo's local-only
|
||||
`feat/adopt-zb-theme` branch (OtOpcUa `11de14d`, ScadaBridge `58352a6`, MxGateway `73e54e2` — committed,
|
||||
spec+code reviewed, **not yet merged/pushed**). The `0.1.0 → 0.2.0` bump first promoted nav-expand persistence
|
||||
into the kit (`NavRailSection.Key`/`data-nav-key` + a localStorage `nav-state.js` enhancer emitted by a new
|
||||
`<ThemeScripts/>`), so all three apps share one persistence mechanism (OtOpcUa's bespoke cookie/JS-interop nav
|
||||
island retired); MxGateway additionally gained a net-new Blazor `<LoginCard>` `/login` page over its existing
|
||||
hardened endpoint. Per-app result in [`components/ui-theme/GAPS.md`](components/ui-theme/GAPS.md).
|
||||
Build/test from `ZB.MOM.WW.Theme/`: `dotnet test`. Consumer matrix: all three apps consume
|
||||
the single `ZB.MOM.WW.Theme` package (OtOpcUa AdminUI, MxGateway Server, ScadaBridge Host + CentralUI).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.0</Version>
|
||||
<Version>0.2.0</Version>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@* Components/NavRailSection.razor — CSS-only collapsible (no JS, works in static SSR).
|
||||
Apps that want cookie-persisted expand state keep their own interactive NavSection. *@
|
||||
@namespace ZB.MOM.WW.Theme
|
||||
<details class="rail-section" open="@Expanded">
|
||||
<details class="rail-section" open="@Expanded" data-nav-key="@ResolvedKey">
|
||||
<summary class="rail-eyebrow-toggle">@Title</summary>
|
||||
<div class="rail-section-body">@ChildContent</div>
|
||||
</details>
|
||||
@@ -18,8 +18,24 @@
|
||||
/// </summary>
|
||||
[Parameter] public bool Expanded { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Stable identifier used to persist this section's open/closed state in
|
||||
/// localStorage (via the kit's nav-state.js). Defaults to a slug of <see cref="Title"/>.
|
||||
/// </summary>
|
||||
[Parameter] public string? Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Section content — typically <see cref="NavRailItem"/> children.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private string ResolvedKey => string.IsNullOrWhiteSpace(Key) ? Slug(Title) : Key!;
|
||||
|
||||
private static string Slug(string? s)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(s)) return string.Empty;
|
||||
var chars = s.Trim().ToLowerInvariant()
|
||||
.Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray();
|
||||
return string.Join('-', new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@namespace ZB.MOM.WW.Theme
|
||||
@* Components/ThemeScripts.razor — drop before </body>. Emits the kit's nav-state
|
||||
enhancer that persists NavRailSection open/closed state in localStorage. *@
|
||||
<script src="_content/ZB.MOM.WW.Theme/js/nav-state.js" defer></script>
|
||||
@@ -0,0 +1,26 @@
|
||||
// ZB.MOM.WW.Theme nav-state.js — persists <details data-nav-key> open/closed
|
||||
// state in localStorage so NavRailSection expand state survives navigation and
|
||||
// reloads. Pure client-side; works with static Blazor SSR. Keyed per section.
|
||||
// localStorage keys are prefixed with "zbnav:" to avoid collisions.
|
||||
(function () {
|
||||
var PREFIX = "zbnav:";
|
||||
var INIT_ATTR = "data-zbnav-initialized";
|
||||
function apply() {
|
||||
document.querySelectorAll("details.rail-section[data-nav-key]").forEach(function (el) {
|
||||
if (el.hasAttribute(INIT_ATTR)) return; // already wired — avoid duplicate listeners
|
||||
el.setAttribute(INIT_ATTR, "");
|
||||
var key = PREFIX + el.getAttribute("data-nav-key");
|
||||
var saved = null;
|
||||
try { saved = window.localStorage.getItem(key); } catch (e) { return; }
|
||||
if (saved === "1") el.open = true;
|
||||
else if (saved === "0") el.open = false;
|
||||
el.addEventListener("toggle", function () {
|
||||
try { window.localStorage.setItem(key, el.open ? "1" : "0"); } catch (e) { /* ignore */ }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (document.readyState === "loading")
|
||||
document.addEventListener("DOMContentLoaded", apply);
|
||||
else
|
||||
apply();
|
||||
})();
|
||||
@@ -55,4 +55,40 @@ public class NavRailTests : TestContext
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.False(cut.Find("details.rail-section").HasAttribute("open"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NavRailSection_emits_data_nav_key_slug_from_title_by_default()
|
||||
{
|
||||
var cut = RenderComponent<NavRailSection>(p => p
|
||||
.Add(x => x.Title, "Site Calls")
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.Equal("site-calls", cut.Find("details.rail-section").GetAttribute("data-nav-key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NavRailSection_emits_explicit_key_when_supplied()
|
||||
{
|
||||
var cut = RenderComponent<NavRailSection>(p => p
|
||||
.Add(x => x.Title, "Navigation").Add(x => x.Key, "nav")
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.Equal("nav", cut.Find("details.rail-section").GetAttribute("data-nav-key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NavRailSection_whitespace_only_title_yields_empty_data_nav_key()
|
||||
{
|
||||
var cut = RenderComponent<NavRailSection>(p => p
|
||||
.Add(x => x.Title, " ")
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.Equal("", cut.Find("details.rail-section").GetAttribute("data-nav-key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NavRailSection_slug_preserves_unicode_letters()
|
||||
{
|
||||
var cut = RenderComponent<NavRailSection>(p => p
|
||||
.Add(x => x.Title, "Café")
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.Equal("café", cut.Find("details.rail-section").GetAttribute("data-nav-key"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ public class StaticAssetsTests
|
||||
public void Fonts_are_vendored(string file) =>
|
||||
Assert.True(File.Exists(Path.Combine(Wwwroot, "fonts", file)));
|
||||
|
||||
[Fact]
|
||||
public void NavStateScript_ships() =>
|
||||
Assert.True(File.Exists(Path.Combine(Wwwroot, "js", "nav-state.js")));
|
||||
|
||||
// Theme-002: .chip-idle pairs the idle background with the matching --idle
|
||||
// foreground token (per DESIGN-TOKENS.md), not --ink-soft.
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ZB.MOM.WW.Theme.Tests;
|
||||
|
||||
public class ThemeScriptsTests : TestContext
|
||||
{
|
||||
[Fact]
|
||||
public void ThemeScripts_emits_nav_state_script_tag()
|
||||
{
|
||||
var cut = RenderComponent<ThemeScripts>();
|
||||
var script = cut.Find("script");
|
||||
Assert.Equal("_content/ZB.MOM.WW.Theme/js/nav-state.js", script.GetAttribute("src"));
|
||||
Assert.True(script.HasAttribute("defer"));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,23 @@
|
||||
Divergence of each project from [`spec/SPEC.md`](spec/SPEC.md), and the ordered backlog to
|
||||
reach adoption of the `ZB.MOM.WW.Theme` shared RCL. Status legend: ⛔ gap · 🟡 partial · ✅ matches.
|
||||
|
||||
> **✅ ADOPTED 2026-06-03 (local-only).** Backlog #2–#4 implemented across all three apps on each repo's
|
||||
> **`feat/adopt-zb-theme`** branch — full canonical cutover (SPEC §7): `<ThemeHead/>`/`<ThemeScripts/>`,
|
||||
> thin `MainLayout` → `<ThemeShell>` + `NavRailItem`/`NavRailSection`, per-app `theme.css`/IBM-Plex fonts/
|
||||
> `nav-state.js` deleted, `<LoginCard>` sign-in, and `StatusPill` (OtOpcUa's dead `StatusBadge` deleted;
|
||||
> MxGateway's `StatusBadge` redirected to a thin `StatusPill` adapter; inline domain `.chip-*` kept as page
|
||||
> content per §6). **Library first enhanced to `0.2.0`** — nav-expand persistence promoted INTO the kit
|
||||
> (`NavRailSection.Key` → `data-nav-key` + a localStorage `nav-state.js` enhancer emitted by a new
|
||||
> `<ThemeScripts/>`), so all three apps get uniform persistence from one source (OtOpcUa's bespoke
|
||||
> cookie/JS-interop nav island retired). 0.2.0 published to the Gitea feed; 44 bUnit tests. **MxGateway
|
||||
> additionally gained a net-new Blazor `<LoginCard>` `/login` page** reusing its existing hardened
|
||||
> `POST /login` endpoint (antiforgery + `SanitizeReturnUrl` + `SignInAsync` preserved). Every task spec+code
|
||||
> reviewed (high-risk via serial spec→code; the MxGateway login via an Opus security review). Branch heads:
|
||||
> OtOpcUa `11de14d`, ScadaBridge `58352a6`, MxGateway `73e54e2` — **committed local-only, not yet merged/pushed**.
|
||||
> Plan: `docs/plans/2026-06-03-ui-theme-adoption*.md`. The ⛔/🟡 cells below describe the PRE-adoption
|
||||
> divergence (kept for history). Deferred follow-ups: prune now-dead `.sidebar`/`.nav-link` residual from each
|
||||
> app's kept `site.css`; a kit-side `layout.css` `calc(100vh - 3.3rem)` review.
|
||||
|
||||
---
|
||||
|
||||
## Divergence vs spec
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# Shared library: `ZB.MOM.WW.Theme`
|
||||
|
||||
**Status: Built (`0.1.0`).** The RCL lives at
|
||||
[`scadaproj/ZB.MOM.WW.Theme/`](../../../ZB.MOM.WW.Theme/) — built and tested. Adoption
|
||||
by the three apps is follow-on, tracked in [`../GAPS.md`](../GAPS.md). Realizes
|
||||
[`../spec/SPEC.md`](../spec/SPEC.md).
|
||||
**Status: Built + Published + Adopted (`0.2.0`).** The RCL lives at
|
||||
[`scadaproj/ZB.MOM.WW.Theme/`](../../../ZB.MOM.WW.Theme/) — built, tested (44 bUnit tests), and
|
||||
**published to the Gitea NuGet feed**. **Adopted across all three apps on 2026-06-03** (local-only
|
||||
`feat/adopt-zb-theme` branches; see [`../GAPS.md`](../GAPS.md)). Realizes [`../spec/SPEC.md`](../spec/SPEC.md).
|
||||
|
||||
`0.2.0` adds **shared nav-expand persistence**: `NavRailSection` gained a `Key` parameter (emitted as
|
||||
`data-nav-key`, defaulting to a slug of `Title`), a vendored `wwwroot/js/nav-state.js` localStorage enhancer
|
||||
(keyed by `data-nav-key`, prefix `zbnav:`, idempotent), and a new **`ThemeScripts`** component (sibling to
|
||||
`ThemeHead`) that emits the enhancer `<script defer>` before `</body>`. This lets every app persist nav
|
||||
expand-state from one shared, static-SSR-friendly mechanism (no per-app cookie/JS-interop island).
|
||||
|
||||
---
|
||||
|
||||
@@ -16,12 +22,14 @@ tokens-only or components-only consumers; all three apps consume the full kit.
|
||||
|---|---|---|
|
||||
| `ZB.MOM.WW.Theme` | `net10.0` Razor Class Library | Tokens + fonts + layout CSS + all components |
|
||||
|
||||
Published to the Gitea NuGet feed; `Version 0.1.0`. SemVer — token changes are
|
||||
breaking (major bump). Build from `scadaproj/ZB.MOM.WW.Theme/`:
|
||||
Published to the Gitea NuGet feed; `Version 0.2.0`. SemVer — token changes are
|
||||
breaking (major bump); the `0.1.0 → 0.2.0` bump added nav persistence (`NavRailSection.Key` +
|
||||
`ThemeScripts` + `nav-state.js`) additively. Build from `scadaproj/ZB.MOM.WW.Theme/`:
|
||||
```bash
|
||||
dotnet build -c Release # 0 warnings (TreatWarningsAsErrors)
|
||||
dotnet test # 32 bUnit tests
|
||||
./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.0.1.0.nupkg
|
||||
dotnet test # 44 bUnit tests
|
||||
./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.0.2.0.nupkg
|
||||
GITEA_NUGET_SOURCE=… GITEA_NUGET_KEY=… ./build/push.sh # publish to the Gitea feed
|
||||
```
|
||||
|
||||
---
|
||||
@@ -72,6 +80,18 @@ Place in `App.razor` `<head>` **after** the app's Bootstrap link.
|
||||
|
||||
---
|
||||
|
||||
### `ThemeScripts`
|
||||
|
||||
Emits the nav-state localStorage enhancer `<script src="_content/ZB.MOM.WW.Theme/js/nav-state.js" defer>`.
|
||||
No parameters. Place in `App.razor` **before `</body>`**. Persists each `NavRailSection`'s open/closed
|
||||
state (keyed by its `data-nav-key`) across navigation and reloads; pure client-side, works in static SSR.
|
||||
|
||||
```razor
|
||||
<ThemeScripts />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ThemeShell`
|
||||
|
||||
Canonical side-rail chassis. **Not a `LayoutComponentBase`** — delegated to from the app's
|
||||
@@ -134,14 +154,15 @@ One rail navigation link. Wraps Blazor `<NavLink class="rail-link">`.
|
||||
|
||||
### `NavRailSection`
|
||||
|
||||
Collapsible nav section group using CSS-only `<details open>` — no JavaScript, works in
|
||||
static Blazor SSR. Apps that need interactive cookie-persisted expand state may keep a
|
||||
bespoke interactive `NavSection` alongside this.
|
||||
Collapsible nav section group using CSS-only `<details open>` — no JavaScript required. Open/closed
|
||||
state is persisted in localStorage by `<ThemeScripts/>` (keyed by `Key` → `data-nav-key`); works in
|
||||
static Blazor SSR.
|
||||
|
||||
| Parameter | Type | Required | Default | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `Title` | `string` | Yes | — | Eyebrow label |
|
||||
| `Expanded` | `bool` | No | `true` | Initial open state |
|
||||
| `Key` | `string?` | No | slug of `Title` | Stable persistence key, emitted as `data-nav-key` |
|
||||
| `Expanded` | `bool` | No | `true` | Initial open state (before localStorage restore) |
|
||||
| `ChildContent` | `RenderFragment?` | No | `null` | `NavRailItem` children |
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# UI-Theme Adoption — Design
|
||||
|
||||
**Date:** 2026-06-03
|
||||
**Status:** Approved (brainstorming complete) — ready for `writing-plans`.
|
||||
**Component:** UI Theme (`ZB.MOM.WW.Theme` shared RCL).
|
||||
**Goal:** Adopt the shared `ZB.MOM.WW.Theme` Razor Class Library across all three sister
|
||||
apps (OtOpcUa AdminUI, MxAccessGateway Dashboard, ScadaBridge CentralUI + Host) via a
|
||||
**full canonical cutover** (SPEC §7), after first **promoting nav-expand persistence into
|
||||
the kit** so every app gets it from one shared mechanism.
|
||||
|
||||
> This is the UI-theme analogue of the completed Auth+Audit normalization
|
||||
> (`docs/plans/2026-06-02-auth-audit-normalization*.md`). It is **UI-only**: no data
|
||||
> contracts, no DB migrations, no wire protocols. The dominant risk is **visual
|
||||
> regression**, not data corruption.
|
||||
|
||||
---
|
||||
|
||||
## 0. Verified starting state (2026-06-03)
|
||||
|
||||
Independently verified (the component docs were optimistic — cf. memory
|
||||
`component-status-claims-are-optimistic`):
|
||||
|
||||
- **Library is real but unpublished and unadopted.** `ZB.MOM.WW.Theme/` holds all 10
|
||||
components + a Release `0.1.0` nupkg, but the Gitea feed returns **HTTP 404** for the
|
||||
package and **no app references it**. The shared-contract's "Published to the Gitea NuGet
|
||||
feed" is aspirational. → This is a clean **publish + adopt**.
|
||||
- **Library is plain files tracked by `scadaproj`** (not a nested git repo) — library
|
||||
changes commit in `scadaproj` (cf. memory `shared-libs-are-plain-files-not-nested-repos`).
|
||||
- **Per-app surface** matches `components/ui-theme/GAPS.md`:
|
||||
- **OtOpcUa AdminUI** — already side-rail (`.app-shell`/`.side-rail`/`.rail-link`);
|
||||
interactive `NavSidebar` island (`@rendermode InteractiveServer`) holding `_expanded`,
|
||||
persisted via JS interop (`window.navState.get/.set`) to the `otopcua_nav` cookie
|
||||
(comma-separated section ids, 1-yr, `SameSite=Lax`); bespoke `StatusBadge`; static-POST
|
||||
`Login.razor`; own `theme.css` + vendored fonts. *Lowest risk.*
|
||||
- **ScadaBridge CentralUI** — `.sidebar`/`.nav-link`/`<ul><li>` (`NavMenu` + `NavSection`);
|
||||
`Login.razor` + `LoginLayout`; own `theme.css`; Host owns `App.razor`. *Medium risk.*
|
||||
- **MxAccessGateway Dashboard** — combined `MainLayout` (~210 lines); `.sidebar`/`.nav-link`;
|
||||
`StatusBadge`; **no Blazor login page** (server-redirect); own `theme.css` (font path is
|
||||
absolute `/fonts/…`, not portable). *Highest risk.*
|
||||
|
||||
---
|
||||
|
||||
## 1. Decisions (locked during brainstorming)
|
||||
|
||||
| # | Decision | Choice |
|
||||
|---|---|---|
|
||||
| D1 | Adoption depth | **A — Full canonical cutover** (SPEC §7 acceptance, all three apps) |
|
||||
| D2 | Nav persistence | **On all apps, via one shared kit mechanism** (not bespoke per app) |
|
||||
| D3 | Persistence implementation | **CSS `<details>` + localStorage enhancer** (recommended over promoting OtOpcUa's interactive-island+cookie) |
|
||||
| D4 | MxGateway login | **Add a new `<LoginCard>` Blazor login page** (the higher-risk consistency option) |
|
||||
| D5 | Delivery model | **Same as Auth/Audit** — `feat/adopt-zb-theme` per app, local-only, then fast-forward merge to each repo's default + push to gitea on explicit go; scadaproj docs on `docs/ui-theme-adoption` |
|
||||
| D6 | Publish | **Publish the (enhanced) RCL to the Gitea feed first**, then adopt (needs `GITEA_NUGET_KEY`, user-supplied, not persisted) |
|
||||
| D7 | Library version | **Bump `0.1.0 → 0.2.0`** (new feature: persistent nav + `ThemeScripts`); publish `0.2.0` directly (0.1.0 was never released) |
|
||||
| D8 | Accent colors | Preserve each app's current `--accent` value (move the *source* to the RCL, don't shift palettes) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Program shape & sequencing
|
||||
|
||||
A **library-minor-then-adopt waterfall** (same shape as Auth/Audit):
|
||||
|
||||
- **Phase 0 — Library enhancement + publish.** Add shared nav persistence (§3), bump to
|
||||
`0.2.0`, run the bUnit suite, `build/push.sh` to the Gitea feed. Commits in `scadaproj`.
|
||||
- **Phase 1 — OtOpcUa AdminUI** (lowest risk; already side-rail; validates the pattern).
|
||||
- **Phase 2 — ScadaBridge CentralUI + Host** (medium; class migration + AuthorizeView nav).
|
||||
- **Phase 3 — MxAccessGateway Dashboard** (highest; split combined layout **and** add the
|
||||
net-new `LoginCard` page).
|
||||
- **Phase 4 — scadaproj docs + memory** (GAPS adoption banner; CLAUDE.md ui-theme row →
|
||||
*Adopted*; shared-contract → *Published 0.2.0*; memory note).
|
||||
|
||||
**Execution:** subagent-driven, classification-driven reviews (trivial→none; small→code;
|
||||
standard→spec∥code parallel; high-risk→serial spec→code + final integration review).
|
||||
|
||||
**Delivery:** `feat/adopt-zb-theme` branch per app, local-only; full build+test green per
|
||||
repo; fast-forward merge to each default + push to gitea on the user's explicit go.
|
||||
|
||||
---
|
||||
|
||||
## 3. Library enhancement: shared nav persistence (Phase 0)
|
||||
|
||||
Promote **one** shared mechanism into the kit — a simpler generalization of OtOpcUa's
|
||||
proven cookie+interop approach.
|
||||
|
||||
**Mechanism — CSS `<details>` + localStorage enhancer:**
|
||||
|
||||
- `NavRailSection` stays the static-SSR-friendly `<details class="rail-section" open="@Expanded">`
|
||||
it already is. It gains a stable **`Key`** parameter (default = a slug of `Title`) emitted
|
||||
as a `data-nav-key` attribute on the `<details>`.
|
||||
- New vendored asset `wwwroot/js/nav-state.js` in the RCL: on `DOMContentLoaded`, for each
|
||||
`[data-nav-key]`, read `localStorage` and set `el.open`; attach a `toggle` listener that
|
||||
writes `el.open` back to `localStorage` keyed by `data-nav-key`. Pure client-side
|
||||
progressive enhancement — no circuit, no server round-trip.
|
||||
- New `<ThemeScripts/>` component (sibling to `ThemeHead`) emits
|
||||
`<script src="_content/ZB.MOM.WW.Theme/js/nav-state.js" defer></script>`, placed before
|
||||
`</body>`.
|
||||
|
||||
**Why localStorage over promoting OtOpcUa's island+cookie:** keeps the kit
|
||||
**static-SSR-friendly** (no forced `InteractiveServer` island per app), one shared file,
|
||||
uniform across all three. It *simplifies* OtOpcUa — retiring its interactive `NavSidebar`
|
||||
island + `nav-state.js` + `otopcua_nav` cookie in favor of the shared enhancer. localStorage
|
||||
is per-browser/origin (same effective scope as the old cookie) and is never read
|
||||
server-side today, so nothing is lost.
|
||||
|
||||
**Trade-off:** a brief flash-of-default-state on first paint (localStorage isn't readable
|
||||
server-side, so sections render at their server default and JS corrects after load).
|
||||
Negligible for a nav rail. (If zero-flash were required, the alternative is a server-read
|
||||
cookie — rejected as more kit coupling.)
|
||||
|
||||
**Version:** `0.1.0 → 0.2.0` (additive feature). **Tests:** extend the bUnit suite —
|
||||
`NavRailSection` emits `data-nav-key` (derived slug + explicit `Key`); `ThemeScripts` emits
|
||||
the script tag. JS runtime behavior is covered by the per-app manual checklist (§5), since
|
||||
bUnit has no JS engine.
|
||||
|
||||
---
|
||||
|
||||
## 4. Per-app adoption scope (full canonical cutover)
|
||||
|
||||
Each app, per SPEC §7: add `PackageReference ZB.MOM.WW.Theme 0.2.0` + `@using ZB.MOM.WW.Theme`
|
||||
in `_Imports.razor`; `<ThemeHead/>` in `App.razor` `<head>` after Bootstrap + `<ThemeScripts/>`
|
||||
before `</body>`; **delete the app's `theme.css` + vendored IBM Plex `.woff2` fonts**; replace
|
||||
`MainLayout` with the thin delegation to `<ThemeShell Product=… Accent=…>`; rebuild nav with
|
||||
`NavRailItem`/`NavRailSection`; `StatusBadge`→`<StatusPill>`; login → `<LoginCard>`; **keep**
|
||||
each app's `site.css` page-layout residual + scoped `.razor.css` unchanged. `--accent`
|
||||
preserves each app's current value (D8).
|
||||
|
||||
| App | Notable specifics | Risk |
|
||||
|---|---|---|
|
||||
| **OtOpcUa** AdminUI | Already-correct rail classes (RCL `layout.css` matches). Retire `NavSidebar` island + `nav-state.js` + `otopcua_nav` cookie → kit `NavRailSection`/`NavRailItem` + shared enhancer. `RailFooter` = the existing `AuthorizeView` session block. `StatusBadge`→`StatusPill`. `Login.razor`→`LoginCard` (keep static POST, `<AntiforgeryToken/>`, server-validate `ReturnUrl`). | Low–Med |
|
||||
| **ScadaBridge** CentralUI + Host | `.sidebar`/`.nav-link`/`<ul><li>` (`NavMenu`+`NavSection`) → kit nav (class migration throughout). Verify `<AuthorizeView>` policy-gated sections render/hide under static SSR (GAPS open Q). `<ThemeHead/>`/`<ThemeScripts/>` go in Host's `App.razor`. `StatusBadge`/inline `.chip-*`→`StatusPill`. `Login.razor`+`LoginLayout`→`LoginCard`. | Med |
|
||||
| **MxGateway** Dashboard | Split combined ~210-line `MainLayout` → thin `MainLayout` + `<ThemeShell>` (nav extracted into the `Nav` slot). `.sidebar`/`.nav-link`→rail classes; portable font path fixed by RCL. `StatusBadge`→`StatusPill`. **Add a new `/login` Blazor page** using `<LoginCard>` posting to a `/auth/login` endpoint wired to the app's existing `ZB.MOM.WW.Auth` LDAP service + dashboard cookie `SignInAsync` (mirror OtOpcUa/ScadaBridge static-POST login). Verify the server auth-redirect now lands on this page. | **High** |
|
||||
|
||||
---
|
||||
|
||||
## 5. Delivery, risk & verification
|
||||
|
||||
- **Build/test gate per repo:** `dotnet build` + the full suite green before merge. Baseline
|
||||
the **known pre-existing reds** first and do not chase them (ScadaBridge IntegrationTests
|
||||
×11 needing live LDAP/SQL/SMTP + flaky `StaleTagMonitor` timer tests; MxGateway 3 FakeWorker
|
||||
tests) — only regressions introduced by this work count.
|
||||
- **Visual regression is the real risk** — a green build does not prove the chrome looks
|
||||
right. Verification per app = a structured manual checklist:
|
||||
1. Rail renders at `lg`+ and collapses to a hamburger toggle below `lg`.
|
||||
2. Nav expand-state persists across navigations and a full reload (shared enhancer).
|
||||
3. `StatusPill` renders correctly in all five states (`Ok`/`Warn`/`Bad`/`Idle`/`Info`).
|
||||
4. Login posts, round-trips `ReturnUrl` safely (server-validated), shows errors.
|
||||
5. IBM Plex fonts load from `_content/ZB.MOM.WW.Theme/fonts/…` (no 404; OtOpcUa's latent
|
||||
font 404 is fixed).
|
||||
- **Optional browser smoke pass:** run each app locally and drive a Claude-in-Chrome smoke
|
||||
pass (screenshots of shell + login) before merge — included only if the user opts in;
|
||||
otherwise the checklist above is run manually.
|
||||
- **MxGateway `/login`** is auth-facing and net-new → `high-risk` classification (serial
|
||||
spec→code review + final integration review).
|
||||
|
||||
---
|
||||
|
||||
## 6. Acceptance (per app)
|
||||
|
||||
Mirrors SPEC §7: (1) `ZB.MOM.WW.Theme 0.2.0` referenced + in `_Imports.razor`; (2)
|
||||
`<ThemeHead/>` after Bootstrap and per-app `theme.css`/fonts deleted; (3) `MainLayout` is the
|
||||
thin `ThemeShell` delegation; (4) nav rebuilt with `NavRailItem`/`NavRailSection` (+ shared
|
||||
persistence via `<ThemeScripts/>`); (5) local `StatusBadge`/`.chip-*` removed → `<StatusPill>`;
|
||||
(6) login is `<LoginCard>` (static POST, `<AntiforgeryToken/>`, server-validated `ReturnUrl`)
|
||||
— including MxGateway's net-new page; (7) `site.css` residual + scoped `.razor.css` kept.
|
||||
|
||||
---
|
||||
|
||||
## 7. Out of scope
|
||||
|
||||
Per SPEC §0/§6: each app's `site.css` page-layout residual, route/page content, scoped
|
||||
`.razor.css`, authorization logic. The kit owns *chrome and tokens*, not domain screens.
|
||||
No new data grids/modals/toasts (YAGNI). Bootstrap stays per-app (not vendored by the kit).
|
||||
@@ -0,0 +1,643 @@
|
||||
# UI-Theme Adoption Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Enhance the shared `ZB.MOM.WW.Theme` RCL with cross-app nav-expand persistence (bump `0.2.0`, publish to the Gitea feed), then adopt it via full canonical cutover across OtOpcUa AdminUI, ScadaBridge CentralUI+Host, and MxAccessGateway Dashboard.
|
||||
|
||||
**Architecture:** A library-minor-then-adopt waterfall (same shape as the completed Auth/Audit normalization). Phase 0 enhances + publishes the kit. Phases 1–3 are **independent per-repo cutovers** (each on its own `feat/adopt-zb-theme` branch, local-only) ordered by risk. Phase 4 updates scadaproj docs + memory. UI-only — no data contracts, no DB migrations; the dominant risk is **visual regression**, mitigated by per-app build+test gates and a manual visual checklist.
|
||||
|
||||
**Tech Stack:** .NET 10, Blazor SSR, Razor Class Library, bUnit/xUnit, Bootstrap 5, NuGet central package management (OtOpcUa/ScadaBridge) / per-project versions (MxGateway), Gitea NuGet feed.
|
||||
|
||||
**Design:** [`2026-06-03-ui-theme-adoption-design.md`](2026-06-03-ui-theme-adoption-design.md). Decisions D1–D8 there are authoritative.
|
||||
|
||||
---
|
||||
|
||||
## Conventions for the executor
|
||||
|
||||
- **Delivery:** scadaproj library + docs changes (Phases 0, 4) commit on the existing `docs/ui-theme-adoption` branch. Each app (Phases 1–3) gets its own `feat/adopt-zb-theme` branch, **committed local-only, never pushed** until the user explicitly authorizes merge+push (same model as Auth/Audit).
|
||||
- **Per-repo green gate:** before declaring an app's phase done, run `dotnet build` + that repo's full test suite. **Baseline known pre-existing reds first** and do not chase them: ScadaBridge IntegrationTests ×11 (need live LDAP/SQL/SMTP), `PartitionPurgeTests.EndToEnd`, flaky `StaleTagMonitor` timer tests; MxGateway 3 FakeWorker tests. Only regressions introduced by this work count.
|
||||
- **Cutover invariant (all apps):** the kit's `theme.css`/`layout.css` define `--*` tokens, the side-rail layout, and the `.chip`/`.chip-ok|warn|bad|idle|info` status classes. Before deleting an app's `wwwroot/css/theme.css`, **diff it against the kit's `theme.css`/`layout.css` and migrate any app-only rules** (e.g. OtOpcUa's `.chip-alert`/`.chip-caution`) into that app's `site.css`. The app's `site.css` page-layout residual and scoped `.razor.css` stay.
|
||||
- **Status policy (per SPEC §6/§7):** inline `.chip-*` spans and Bootstrap `.badge` in *domain pages* are page content — they keep working under kit CSS and are **not** rewritten. Only a bespoke status *component* gets removed/redirected to `<StatusPill>`.
|
||||
- **Cross-repo parallelism:** Phases 1, 2, 3 touch disjoint repos and are mutually independent — they MAY run concurrently, but are listed in risk order (OtOpcUa → ScadaBridge → MxGateway). All three are blocked by Task 0.4 (published package).
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Library enhancement + publish (scadaproj, branch `docs/ui-theme-adoption`)
|
||||
|
||||
### Task 0.1: NavRailSection persistence key
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** Task 0.2
|
||||
|
||||
**Files:**
|
||||
- Modify: `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor`
|
||||
- Test: `ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/NavRailTests.cs`
|
||||
|
||||
**Context:** `NavRailSection` renders `<details class="rail-section" open="@Expanded"><summary class="rail-eyebrow-toggle">@Title</summary>…`. Add an optional `Key` parameter (default = a stable slug of `Title`) emitted as `data-nav-key` on the `<details>` so the localStorage enhancer (Task 0.2) can persist per-section open state.
|
||||
|
||||
**Step 1 — failing tests** in `NavRailTests.cs`:
|
||||
```csharp
|
||||
[Fact]
|
||||
public void NavRailSection_emits_data_nav_key_slug_from_title_by_default()
|
||||
{
|
||||
var cut = RenderComponent<NavRailSection>(p => p
|
||||
.Add(x => x.Title, "Site Calls")
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.Equal("site-calls", cut.Find("details.rail-section").GetAttribute("data-nav-key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NavRailSection_emits_explicit_key_when_supplied()
|
||||
{
|
||||
var cut = RenderComponent<NavRailSection>(p => p
|
||||
.Add(x => x.Title, "Navigation").Add(x => x.Key, "nav")
|
||||
.AddChildContent("<a class='rail-link'>X</a>"));
|
||||
Assert.Equal("nav", cut.Find("details.rail-section").GetAttribute("data-nav-key"));
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2 — run, expect FAIL** (no `Key`/`data-nav-key`):
|
||||
`dotnet test ZB.MOM.WW.Theme/ --filter "FullyQualifiedName~NavRailSection_emits"`
|
||||
|
||||
**Step 3 — implement.** Edit `NavRailSection.razor`:
|
||||
```razor
|
||||
@namespace ZB.MOM.WW.Theme
|
||||
<details class="rail-section" open="@Expanded" data-nav-key="@ResolvedKey">
|
||||
<summary class="rail-eyebrow-toggle">@Title</summary>
|
||||
<div class="rail-section-body">@ChildContent</div>
|
||||
</details>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string Title { get; set; } = string.Empty;
|
||||
[Parameter] public bool Expanded { get; set; } = true;
|
||||
|
||||
/// <summary>Stable identifier used to persist this section's open/closed state in
|
||||
/// localStorage (via the kit's nav-state.js). Defaults to a slug of <see cref="Title"/>.</summary>
|
||||
[Parameter] public string? Key { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private string ResolvedKey => string.IsNullOrWhiteSpace(Key) ? Slug(Title) : Key!;
|
||||
|
||||
private static string Slug(string s)
|
||||
{
|
||||
var chars = s.Trim().ToLowerInvariant()
|
||||
.Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray();
|
||||
return string.Join('-', new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4 — run, expect PASS** (plus the existing NavRail tests stay green).
|
||||
|
||||
**Step 5 — commit:** `git add -A && git commit -m "feat(theme): NavRailSection data-nav-key for persistence"`
|
||||
|
||||
---
|
||||
|
||||
### Task 0.2: localStorage nav enhancer + ThemeScripts
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 0.1
|
||||
|
||||
**Files:**
|
||||
- Create: `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js`
|
||||
- Create: `ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeScripts.razor`
|
||||
- Test: `ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/ThemeScriptsTests.cs` (new)
|
||||
- Test: `ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/StaticAssetsTests.cs` (extend)
|
||||
|
||||
**Step 1 — create `wwwroot/js/nav-state.js`** (progressive enhancement; no framework):
|
||||
```javascript
|
||||
// ZB.MOM.WW.Theme nav-state.js — persists <details data-nav-key> open/closed
|
||||
// state in localStorage so NavRailSection expand state survives navigation and
|
||||
// reloads. Pure client-side; works with static Blazor SSR. Keyed per section.
|
||||
(function () {
|
||||
var PREFIX = "zbnav:";
|
||||
function apply() {
|
||||
document.querySelectorAll("details.rail-section[data-nav-key]").forEach(function (el) {
|
||||
var key = PREFIX + el.getAttribute("data-nav-key");
|
||||
var saved = null;
|
||||
try { saved = window.localStorage.getItem(key); } catch (e) { return; }
|
||||
if (saved === "1") el.open = true;
|
||||
else if (saved === "0") el.open = false;
|
||||
el.addEventListener("toggle", function () {
|
||||
try { window.localStorage.setItem(key, el.open ? "1" : "0"); } catch (e) { /* ignore */ }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (document.readyState === "loading")
|
||||
document.addEventListener("DOMContentLoaded", apply);
|
||||
else
|
||||
apply();
|
||||
})();
|
||||
```
|
||||
|
||||
**Step 2 — create `Components/ThemeScripts.razor`:**
|
||||
```razor
|
||||
@namespace ZB.MOM.WW.Theme
|
||||
@* Components/ThemeScripts.razor — drop before </body>. Emits the kit's nav-state
|
||||
enhancer that persists NavRailSection open/closed state in localStorage. *@
|
||||
<script src="_content/ZB.MOM.WW.Theme/js/nav-state.js" defer></script>
|
||||
```
|
||||
|
||||
**Step 3 — failing tests.** `ThemeScriptsTests.cs`:
|
||||
```csharp
|
||||
namespace ZB.MOM.WW.Theme.Tests;
|
||||
|
||||
public class ThemeScriptsTests : TestContext
|
||||
{
|
||||
[Fact]
|
||||
public void ThemeScripts_emits_nav_state_script_tag()
|
||||
{
|
||||
var cut = RenderComponent<ThemeScripts>();
|
||||
var script = cut.Find("script");
|
||||
Assert.Equal("_content/ZB.MOM.WW.Theme/js/nav-state.js", script.GetAttribute("src"));
|
||||
Assert.True(script.HasAttribute("defer"));
|
||||
}
|
||||
}
|
||||
```
|
||||
In `StaticAssetsTests.cs`, add an assertion that the JS file ships (mirror its existing CSS/font asset checks — read the file first to match its exact assertion style, e.g. verifying the file exists on disk under `wwwroot/js/nav-state.js`).
|
||||
|
||||
**Step 4 — run tests, expect PASS:** `dotnet test ZB.MOM.WW.Theme/`
|
||||
|
||||
**Step 5 — commit:** `git commit -am "feat(theme): ThemeScripts + localStorage nav-state enhancer"`
|
||||
|
||||
---
|
||||
|
||||
### Task 0.3: Version bump 0.2.0 + full suite
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~2 min
|
||||
**Parallelizable with:** none (depends on 0.1, 0.2)
|
||||
|
||||
**Files:**
|
||||
- Modify: `ZB.MOM.WW.Theme/Directory.Build.props:7`
|
||||
|
||||
**Steps:**
|
||||
1. Change `<Version>0.1.0</Version>` → `<Version>0.2.0</Version>`.
|
||||
2. Run `cd ZB.MOM.WW.Theme && dotnet build -c Release` — expect **0 warnings** (TreatWarningsAsErrors).
|
||||
3. Run `dotnet test` — expect all green (38 existing + the new persistence/ThemeScripts tests).
|
||||
4. Commit: `git commit -am "chore(theme): bump 0.1.0 -> 0.2.0 (nav persistence + ThemeScripts)"`
|
||||
|
||||
---
|
||||
|
||||
### Task 0.4: Publish 0.2.0 to Gitea feed
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~2 min (blocks on user-supplied token)
|
||||
**Parallelizable with:** none (depends on 0.3)
|
||||
|
||||
**⚠ Requires the user's `GITEA_NUGET_KEY`** (Gitea token with `package:write`). It is not persisted — ask the user to export it (or run the push command themselves via `! …`). Do not invent or store it.
|
||||
|
||||
**Steps:**
|
||||
1. Confirm 404 pre-state: `curl -s -o /dev/null -w "%{http_code}\n" https://gitea.dohertylan.com/api/packages/dohertj2/nuget/registration/zb.mom.ww.theme/index.json` (expect `404`).
|
||||
2. Publish:
|
||||
```bash
|
||||
cd ZB.MOM.WW.Theme
|
||||
export GITEA_NUGET_SOURCE="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json"
|
||||
export GITEA_NUGET_KEY="<user-supplied>"
|
||||
./build/push.sh
|
||||
```
|
||||
3. Verify published: re-run the curl — expect `200`; confirm version `0.2.0` is listed.
|
||||
4. No commit needed (artifacts are gitignored). Record the publish in the task log.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — OtOpcUa AdminUI cutover (repo `~/Desktop/OtOpcUa`, branch `feat/adopt-zb-theme`)
|
||||
|
||||
> Blocked by Task 0.4. Lowest risk: already side-rail with the kit's exact CSS classes.
|
||||
> **First:** `cd ~/Desktop/OtOpcUa && git checkout -b feat/adopt-zb-theme`.
|
||||
|
||||
### Task 1.1: NuGet wiring + usings
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** none (gates 1.2–1.5)
|
||||
|
||||
**Files:**
|
||||
- Modify: `Directory.Packages.props` (repo root) — add `<PackageVersion Include="ZB.MOM.WW.Theme" Version="0.2.0" />`
|
||||
- Modify: `NuGet.config` (repo root) — under `<packageSource key="dohertj2-gitea">` add `<package pattern="ZB.MOM.WW.Theme" />`
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj` — add `<PackageReference Include="ZB.MOM.WW.Theme" />` (versionless; central PM)
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/_Imports.razor` — add `@using ZB.MOM.WW.Theme`
|
||||
|
||||
**Verify:** `dotnet restore src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj` resolves `ZB.MOM.WW.Theme 0.2.0` from the Gitea feed. Commit.
|
||||
|
||||
### Task 1.2: App.razor — ThemeHead + ThemeScripts
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~2 min
|
||||
**Parallelizable with:** Task 1.3, 1.4, 1.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/App.razor`
|
||||
|
||||
**Edits:** Replace line `…/css/theme.css` `<link>` with `<ThemeHead />` (keep the Bootstrap `<link>` *above* it and the `…/css/site.css` `<link>` *below* it). Replace `<script src="…/js/nav-state.js"></script>` with `<ThemeScripts />`. Keep the bootstrap bundle + `blazor.web.js` scripts. Commit.
|
||||
|
||||
### Task 1.3: Migrate app-only CSS, delete theme.css + fonts + nav-state.js
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** Task 1.2, 1.4, 1.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/wwwroot/css/site.css`
|
||||
- Delete: `wwwroot/css/theme.css`, `wwwroot/fonts/ibm-plex-*.woff2` (×3), `wwwroot/js/nav-state.js`
|
||||
|
||||
**Steps:** Diff `wwwroot/css/theme.css` against the kit's `theme.css`+`layout.css`. Any rule present in the app copy but NOT the kit (notably **`.chip-alert`, `.chip-caution`**, and any app-only tweak) → append to `site.css` under a clearly-commented "App-specific status variants (not in ZB.MOM.WW.Theme)" block. Then delete the four asset files. Keep `wwwroot/js/monaco-loader.js`. Commit.
|
||||
|
||||
### Task 1.4: MainLayout → ThemeShell + kit nav
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 1.2, 1.3, 1.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Layout/MainLayout.razor`
|
||||
- Delete: `Components/Layout/NavSidebar.razor`, `Components/Layout/NavSection.razor`
|
||||
|
||||
**Context:** Replaces the interactive `NavSidebar` island + bespoke `NavSection` with the kit's static `<ThemeShell>` + `NavRailSection`/`NavRailItem` (persistence now comes from `ThemeScripts`). All sections default `Expanded=true`; the URL-based auto-expand behavior is intentionally dropped (D2/D3 — localStorage persistence replaces it). Reproduce the 3 sections / 17 links / footer exactly.
|
||||
|
||||
**Target `MainLayout.razor`:**
|
||||
```razor
|
||||
@inherits LayoutComponentBase
|
||||
<ThemeShell Product="OtOpcUa" Accent="#2f5fd0">
|
||||
<Nav>
|
||||
<NavRailSection Title="Navigation" Key="nav">
|
||||
<NavRailItem Href="/" Text="Overview" Match="NavLinkMatch.All" />
|
||||
<NavRailItem Href="/fleet" Text="Fleet status" />
|
||||
<NavRailItem Href="/hosts" Text="Host status" />
|
||||
<NavRailItem Href="/clusters" Text="Clusters" />
|
||||
<NavRailItem Href="/reservations" Text="Reservations" />
|
||||
<NavRailItem Href="/certificates" Text="Certificates" />
|
||||
<NavRailItem Href="/role-grants" Text="Role grants" />
|
||||
</NavRailSection>
|
||||
<NavRailSection Title="Scripting" Key="scripting">
|
||||
<NavRailItem Href="/virtual-tags" Text="Virtual tags" />
|
||||
<NavRailItem Href="/scripted-alarms" Text="Scripted alarms" />
|
||||
<NavRailItem Href="/scripts" Text="Scripts" />
|
||||
<NavRailItem Href="/script-log" Text="Script log" />
|
||||
</NavRailSection>
|
||||
<NavRailSection Title="Live" Key="live">
|
||||
<NavRailItem Href="/deployments" Text="Deployments" />
|
||||
<NavRailItem Href="/alerts" Text="Alerts" />
|
||||
<NavRailItem Href="/alarms-historian" Text="Alarms historian" />
|
||||
</NavRailSection>
|
||||
</Nav>
|
||||
<RailFooter>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="rail-eyebrow">Session</div>
|
||||
<a class="rail-user" href="/account">@context.User.Identity?.Name</a>
|
||||
<div class="rail-roles">@string.Join(", ", context.User.Claims.Where(c => c.Type.EndsWith("/role")).Select(c => c.Value))</div>
|
||||
<form method="post" action="/auth/logout"><AntiforgeryToken /><button class="rail-btn" type="submit">Sign out</button></form>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<div class="rail-eyebrow">Session</div>
|
||||
<a class="rail-btn" href="/login">Sign in</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</RailFooter>
|
||||
<ChildContent>@Body</ChildContent>
|
||||
</ThemeShell>
|
||||
```
|
||||
**Note:** confirm `ThemeShell` exposes `Nav`/`RailFooter`/`ChildContent` slots and that the hamburger/collapse behavior comes from the kit's `layout.css` (Bootstrap collapse JS already loaded). If the kit shell wraps the rail in its own collapse, drop the app's old hamburger markup (now in the shell). Build the AdminUI project; verify it compiles. Commit.
|
||||
|
||||
### Task 1.5: Delete dead StatusBadge + Login → LoginCard
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** Task 1.2, 1.3, 1.4
|
||||
|
||||
**Files:**
|
||||
- Delete: `Components/Shared/StatusBadge.razor` (verified unused — confirm with a repo grep for `<StatusBadge` returning 0 hits before deleting)
|
||||
- Modify: `Components/Pages/Login.razor`
|
||||
|
||||
**Login target** (preserve static POST to `/auth/login`, the `Error`/`ReturnUrl` query params, and `LoginLayout`):
|
||||
```razor
|
||||
@page "/login"
|
||||
@layout LoginLayout
|
||||
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
||||
<div class="login-wrap rise" style="animation-delay:.02s">
|
||||
<LoginCard Product="OtOpcUa Admin" Action="/auth/login" ReturnUrl="@ReturnUrl" Error="@Error">
|
||||
<AntiforgeryToken />
|
||||
</LoginCard>
|
||||
</div>
|
||||
@code {
|
||||
[SupplyParameterFromQuery] private string? Error { get; set; }
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
}
|
||||
```
|
||||
**Note:** the `/auth/login` endpoint already round-trips `returnUrl` and signs in (unchanged). Confirm `<LoginCard>` renders the username/password fields the endpoint reads (`name="username"`, `name="password"`, `name="returnUrl"`); if its field names differ, set them via LoginCard params or keep the existing `<form>` inside a `<TechCard>` instead. Build; commit.
|
||||
|
||||
### Task 1.6: Build, test, visual checklist (OtOpcUa)
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** none (depends on 1.2–1.5)
|
||||
|
||||
**Steps:** `dotnet build ZB.MOM.WW.OtOpcUa.slnx`; `dotnet test ZB.MOM.WW.OtOpcUa.slnx` (compare against baseline reds). Run the visual checklist (design §5): rail renders ≥lg + hamburger <lg; nav persistence across reload; status chips intact (incl. alert/caution); login posts + returnUrl; IBM Plex fonts load from `_content/ZB.MOM.WW.Theme/fonts/` (the old latent 404 is gone). Report results; do not merge.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — ScadaBridge CentralUI + Host cutover (repo `~/Desktop/ScadaBridge`, branch `feat/adopt-zb-theme`)
|
||||
|
||||
> Blocked by Task 0.4. Independent of Phase 1. **First:** `cd ~/Desktop/ScadaBridge && git checkout -b feat/adopt-zb-theme`.
|
||||
|
||||
### Task 2.1: NuGet wiring + usings
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
|
||||
**Files:**
|
||||
- Modify: `Directory.Packages.props` — add `<PackageVersion Include="ZB.MOM.WW.Theme" Version="0.2.0" />`
|
||||
- Modify: `nuget.config` — under `dohertj2-gitea` add `<package pattern="ZB.MOM.WW.Theme" />`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/ZB.MOM.WW.ScadaBridge.CentralUI.csproj` — add `<PackageReference Include="ZB.MOM.WW.Theme" />`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/_Imports.razor` — add `@using ZB.MOM.WW.Theme`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/_Imports.razor` — add `@using ZB.MOM.WW.Theme` (Host's `App.razor` uses `ThemeHead`/`ThemeScripts`; the RCL flows transitively via the CentralUI project reference)
|
||||
|
||||
**Verify** restore resolves 0.2.0; commit.
|
||||
|
||||
### Task 2.2: Host App.razor — ThemeHead + ThemeScripts
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~2 min
|
||||
**Parallelizable with:** Task 2.3, 2.4, 2.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/Components/App.razor`
|
||||
|
||||
**Edits:** Replace the `_content/ZB.MOM.WW.ScadaBridge.CentralUI/css/theme.css` `<link>` with `<ThemeHead />` (keep Bootstrap + bootstrap-icons links above; keep `/ZB.MOM.WW.ScadaBridge.Host.styles.css` and the CentralUI `site.css` link). Replace the `…CentralUI/js/nav-state.js` `<script>` with `<ThemeScripts />`; keep `treeview-storage.js`, `monaco-init.js`, `audit-grid.js`, `transport.js`, and the bootstrap bundle. Commit.
|
||||
|
||||
### Task 2.3: Migrate app-only CSS, delete theme.css + fonts + nav-state.js
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** Task 2.2, 2.4, 2.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css` (only if the diff surfaces app-only rules)
|
||||
- Delete: `CentralUI/wwwroot/css/theme.css`, `CentralUI/wwwroot/fonts/ibm-plex-*.woff2` (×3), `CentralUI/wwwroot/js/nav-state.js`
|
||||
|
||||
**Steps:** Diff CentralUI `theme.css` vs kit; migrate any app-only rules into `site.css` (ScadaBridge's chips are the standard ok/warn/bad/idle, covered by the kit — expect little/none). Keep the other JS files. Commit.
|
||||
|
||||
### Task 2.4: MainLayout → ThemeShell + kit nav (preserve AuthorizeView gating, DialogHost, SessionExpiry)
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 2.2, 2.3, 2.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/MainLayout.razor`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor`
|
||||
- Delete: `Components/Layout/NavSection.razor` (after NavMenu no longer uses it)
|
||||
|
||||
**Context:** Two non-obvious must-preserves: `MainLayout` hosts `<DialogHost />` and `<SessionExpiry />` — keep both in the thin layout (outside `<ThemeShell>` or in `ChildContent` alongside `@Body`). `NavMenu` wraps its sections in `<AuthorizeView Policy="…">` (RequireAdmin/RequireDesign/RequireDeployment/OperationalAudit + mixed-role children) — these policy guards must wrap the new `NavRailSection`s unchanged.
|
||||
|
||||
**Steps:**
|
||||
1. `MainLayout.razor` → thin delegation:
|
||||
```razor
|
||||
@inherits LayoutComponentBase
|
||||
<ThemeShell Product="ScadaBridge" Accent="#2f5fd0">
|
||||
<Nav><NavMenu /></Nav>
|
||||
<RailFooter>
|
||||
<AuthorizeView><Authorized>
|
||||
<div class="rail-eyebrow">Session</div>
|
||||
<span class="rail-user">@context.User.GetDisplayName()</span>
|
||||
<form method="post" action="/auth/logout" data-enhance="false"><AntiforgeryToken /><button class="rail-btn" type="submit">Sign Out</button></form>
|
||||
</Authorized></AuthorizeView>
|
||||
</RailFooter>
|
||||
<ChildContent>@Body</ChildContent>
|
||||
</ThemeShell>
|
||||
<DialogHost />
|
||||
<SessionExpiry />
|
||||
```
|
||||
(Move the session/sign-out block out of `NavMenu` into `RailFooter`; keep `GetDisplayName()`.)
|
||||
2. Rewrite `NavMenu.razor` body: replace `<nav class="sidebar"><ul class="nav flex-column">…` + `<li><NavLink class="nav-link">` + `<NavSection>` with kit `NavRailSection`/`NavRailItem`, **preserving each `<AuthorizeView Policy="…">` wrapper** around its section. The always-visible Dashboard link becomes a bare `<NavRailItem Href="/" Text="Dashboard" Match="NavLinkMatch.All" />` (outside any section, or in a default section). Reproduce all sections/links from the inventory (Admin, Design, Deployment, Notifications, Site Calls, Monitoring, Audit) and their child links exactly.
|
||||
3. Build CentralUI; **verify `<AuthorizeView>`-wrapped `NavRailSection` renders for an authorized principal and hides for an unauthorized one** under static SSR (GAPS open question) — assert via an existing CentralUI bUnit test or add a focused one. Commit.
|
||||
|
||||
### Task 2.5: Login → LoginCard
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** Task 2.2, 2.3, 2.4
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Login.razor`
|
||||
|
||||
**Target** (preserve static POST to `/auth/login`; the endpoint uses `.DisableAntiforgery()` and always redirects `/` — no `returnUrl`, no antiforgery token needed):
|
||||
```razor
|
||||
@page "/login"
|
||||
@layout LoginLayout
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [AllowAnonymous]
|
||||
<LoginCard Product="ScadaBridge" Action="/auth/login" Error="@ErrorMessage" />
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "error")] public string? ErrorMessage { get; set; }
|
||||
}
|
||||
```
|
||||
Confirm `<LoginCard>`'s field names match what `/auth/login` reads (`username`/`password`). Build; commit.
|
||||
|
||||
### Task 2.6: Build, test, visual checklist (ScadaBridge)
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none (depends on 2.2–2.5)
|
||||
|
||||
**Steps:** `dotnet build ZB.MOM.WW.ScadaBridge.slnx`; run the FULL suite (Host/CentralUI/ManagementService/Transport/ConfigurationDatabase) and compare to baseline reds. Visual checklist incl. policy-gated nav sections show/hide by role, DialogHost + SessionExpiry still function. Report; do not merge.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — MxAccessGateway Dashboard cutover (repo `~/Desktop/MxAccessGateway`, branch `feat/adopt-zb-theme`)
|
||||
|
||||
> Blocked by Task 0.4. Independent of Phases 1–2. Highest risk: combined-layout split + login conversion. **First:** `cd ~/Desktop/MxAccessGateway && git checkout -b feat/adopt-zb-theme`.
|
||||
|
||||
### Task 3.1: NuGet wiring + usings (no central PM)
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj` — add `<PackageReference Include="ZB.MOM.WW.Theme" Version="0.2.0" />` (explicit version; this repo has no `Directory.Packages.props`)
|
||||
- Modify: `NuGet.config` — under `dohertj2-gitea` add `<package pattern="ZB.MOM.WW.Theme" />`
|
||||
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/_Imports.razor` — add `@using ZB.MOM.WW.Theme`
|
||||
|
||||
**Verify** restore resolves 0.2.0; commit.
|
||||
|
||||
### Task 3.2: App.razor — ThemeHead + ThemeScripts
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~2 min
|
||||
**Parallelizable with:** Task 3.3, 3.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/App.razor`
|
||||
|
||||
**Edits:** Replace `<link rel="stylesheet" href="/css/theme.css" />` with `<ThemeHead />` (keep Bootstrap link above + `/css/site.css` below). Replace `<script src="/js/nav-state.js"></script>` with `<ThemeScripts />`. Keep bootstrap bundle + `blazor.web.js`. Commit. *(Note: `<HeadOutlet>`/`<Routes>` keep their `@rendermode="InteractiveServer"`; ThemeHead/ThemeScripts are static markup and unaffected.)*
|
||||
|
||||
### Task 3.3: Migrate app-only CSS, delete theme.css + fonts
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** Task 3.2, 3.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/site.css` (only if diff surfaces app-only rules)
|
||||
- Delete: `wwwroot/css/theme.css`, `wwwroot/fonts/ibm-plex-*.woff2` (×3)
|
||||
|
||||
**Steps:** Diff `theme.css` vs kit; migrate app-only rules to `site.css`. The kit's `@font-face` uses the correct relative path (the app's absolute `/fonts/` path is retired). **Keep** `wwwroot/js/nav-state.js`? No — it is replaced by `ThemeScripts` (Task 3.2 removed its `<script>`); delete `wwwroot/js/nav-state.js` here too. Commit.
|
||||
|
||||
### Task 3.4: Split combined MainLayout → thin MainLayout + ThemeShell
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 3.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor`
|
||||
- Delete: `Dashboard/Components/Layout/NavSection.razor`
|
||||
|
||||
**Context:** The ~211-line combined layout (hamburger + `<nav class="sidebar">` + brand + 3 `NavSection`s + AuthorizeView footer + `<main>`) collapses to a thin `<ThemeShell>` delegation. Reproduce the Runtime / Galaxy / Admin sections + links and the footer (Authorized: user + Sign Out POST `/logout`; NotAuthorized: Sign In `/login`).
|
||||
```razor
|
||||
@inherits LayoutComponentBase
|
||||
<ThemeShell Product="MXAccess Gateway" Accent="#2f5fd0">
|
||||
<Nav>
|
||||
<NavRailItem Href="/" Text="Dashboard" Match="NavLinkMatch.All" />
|
||||
<NavRailSection Title="Runtime" Key="runtime"> … sessions/workers/events/alarms … </NavRailSection>
|
||||
<NavRailSection Title="Galaxy" Key="galaxy"> … repository/browse … </NavRailSection>
|
||||
<NavRailSection Title="Admin" Key="admin"> … API Keys/settings … </NavRailSection>
|
||||
</Nav>
|
||||
<RailFooter>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="rail-eyebrow">Session</div>
|
||||
<span class="rail-user">@context.User.Identity?.Name</span>
|
||||
<form method="post" action="/logout" data-enhance="false"><AntiforgeryToken /><button class="rail-btn" type="submit">Sign Out</button></form>
|
||||
</Authorized>
|
||||
<NotAuthorized><a class="rail-btn" href="/login">Sign In</a></NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</RailFooter>
|
||||
<ChildContent>@Body</ChildContent>
|
||||
</ThemeShell>
|
||||
```
|
||||
Fill the section children from the inventory's exact hrefs/labels. Build the Server project; commit.
|
||||
|
||||
### Task 3.5: StatusBadge → StatusPill adapter
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** Task 3.2, 3.3, 3.4
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Shared/StatusBadge.razor`
|
||||
|
||||
**Decision (documented deviation from SPEC §7.5):** `StatusBadge` is used at 12 call sites with a domain `Text`→class switch. Rather than scatter the text→state mapping across 12 pages, **redirect `StatusBadge` to render `<StatusPill>`** — the bespoke `.chip` rendering moves to the kit; only the app's domain text→state mapping (per-project vocabulary, SPEC §6) remains. Call sites stay unchanged.
|
||||
```razor
|
||||
@* Thin adapter: maps MxGateway runtime state text → kit StatusPill state. *@
|
||||
<StatusPill State="MapState(Text)">@Text</StatusPill>
|
||||
@code {
|
||||
[Parameter] public string? Text { get; set; }
|
||||
private static StatusState MapState(string? t) => t switch
|
||||
{
|
||||
"Ready" or "Healthy" or "Active" => StatusState.Ok,
|
||||
"Creating" or "StartingWorker" or "WaitingForPipe" or "InitializingWorker" or "Closing" or "Stale" or "Degraded" => StatusState.Warn,
|
||||
"Faulted" or "Unavailable" => StatusState.Bad,
|
||||
_ => StatusState.Idle,
|
||||
};
|
||||
}
|
||||
```
|
||||
Confirm `StatusPill` renders its `ChildContent` as the label and emits `chip chip-ok|warn|bad|idle`. Build; commit. *(If the reviewer insists on literal deletion, the fallback is replacing all 12 call sites with `<StatusPill>` + a shared static `MapState` helper — note it but prefer the adapter.)*
|
||||
|
||||
### Task 3.6: Net-new Blazor LoginCard page (reuse existing hardened endpoint)
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** none (depends on 3.4 for layout/usings context)
|
||||
|
||||
**Files:**
|
||||
- Create: `Dashboard/Components/Layout/LoginLayout.razor`
|
||||
- Create: `Dashboard/Components/Pages/Login.razor`
|
||||
- Modify: `Dashboard/DashboardEndpointRouteBuilderExtensions.cs`
|
||||
|
||||
**Context (discovered reality):** MxGateway is NOT login-less — it has a working, hardened login: `POST /login` (`PostLoginAsync`) validates antiforgery, calls `IDashboardAuthenticator.AuthenticateAsync` (LDAP via shared Auth → roles → `ZbClaimTypes` principal), `SignInAsync`, then `LocalRedirect(SanitizeReturnUrl(returnUrl))`. The login *UI* is a raw HTML string from `GetLoginAsync`/`RenderLoginPage`. We swap **only the UI** to a Blazor `<LoginCard>` page; the `POST /login` endpoint and authenticator are reused unchanged. A `<form method="post">` posts natively, so the page's render mode is irrelevant to the POST.
|
||||
|
||||
**Steps:**
|
||||
1. `LoginLayout.razor`: `@inherits LayoutComponentBase` + `@Body` (no rail).
|
||||
2. `Login.razor`:
|
||||
```razor
|
||||
@page "/login"
|
||||
@layout LoginLayout
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [AllowAnonymous]
|
||||
<div class="dashboard-login">
|
||||
<LoginCard Product="MXAccess Gateway" Action="/login" ReturnUrl="@ReturnUrl" Error="@Error">
|
||||
<AntiforgeryToken />
|
||||
</LoginCard>
|
||||
</div>
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "returnUrl")] private string? ReturnUrl { get; set; }
|
||||
[SupplyParameterFromQuery(Name = "error")] private string? Error { get; set; }
|
||||
}
|
||||
```
|
||||
3. In `DashboardEndpointRouteBuilderExtensions.cs`:
|
||||
- **Remove** the `MapGet("/login", … GetLoginAsync)` registration and the `GetLoginAsync` + `RenderLoginPage` helpers (the Blazor route now serves `GET /login`; the component carries `[AllowAnonymous]` to override the `RequireAuthorization(ViewerPolicy)` on `MapRazorComponents<App>()`).
|
||||
- **Change** `PostLoginAsync`'s failure branch from re-rendering HTML to a redirect: `return Results.Redirect($"/login?error={Uri.EscapeDataString(result.FailureMessage ?? "…")}&returnUrl={Uri.EscapeDataString(returnUrl)}");`. Keep antiforgery validation, `SignInAsync`, and the success `LocalRedirect(returnUrl)`.
|
||||
- Keep `MapPost("/login")`, `/logout` (GET+POST), and `/denied` (still uses `RenderPage`).
|
||||
4. Build the Server project. **Verify the full flow:** unauthenticated request to `/` → cookie challenge → `/login` renders the Blazor `<LoginCard>` anonymously → POST authenticates → cookie set → redirect. Commit.
|
||||
|
||||
### Task 3.7: Build, test, login smoke (MxGateway)
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none (depends on 3.2–3.6)
|
||||
|
||||
**Steps:** `dotnet build src/MxGateway.sln` (+ worker x86 if the suite needs it); `dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj` (compare to the 3 baseline FakeWorker reds). Visual + auth smoke: layout renders, nav persists, status pills, **login page renders and a valid/invalid credential round-trips correctly** (the highest-risk surface). Report; do not merge.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — scadaproj docs + memory (branch `docs/ui-theme-adoption`)
|
||||
|
||||
### Task 4.1: Update component docs
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** none (depends on 1.6, 2.6, 3.7)
|
||||
|
||||
**Files:**
|
||||
- Modify: `components/ui-theme/GAPS.md` — add an "✅ ADOPTED 2026-06-03 (local-only)" banner mirroring the Auth/Audit GAPS banners; note persistence promoted to the kit + MxGateway's new LoginCard page.
|
||||
- Modify: `components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md` — status → "Built + Published `0.2.0`"; document `ThemeScripts` + `NavRailSection.Key` + the nav-state.js asset.
|
||||
- Modify: `CLAUDE.md` — UI-Theme component row status → "Adopted (lib `0.2.0`; all 3 apps, local feature branches)"; bump the version/test-count prose (38→ new total).
|
||||
|
||||
Commit.
|
||||
|
||||
### Task 4.2: Update memory
|
||||
|
||||
**Classification:** trivial
|
||||
**Estimated implement time:** ~2 min
|
||||
**Parallelizable with:** Task 4.1
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/dohertj2/.claude/projects/-Users-dohertj2-Desktop-scadaproj/memory/ui-theme-adoption.md` (project memory: scope, 0.2.0, per-app branches local-only, MxGateway login conversion, persistence-in-kit decision; link `[[component-status-claims-are-optimistic]]`, `[[shared-libs-are-plain-files-not-nested-repos]]`).
|
||||
- Modify: `…/memory/MEMORY.md` — add the index line.
|
||||
|
||||
Commit.
|
||||
|
||||
### Task 4.3: Final integration review
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none
|
||||
|
||||
Dispatch a final reviewer across all three `feat/adopt-zb-theme` diffs + the scadaproj Phase 0/4 diff: confirm SPEC §7 acceptance per app, no app-only CSS lost, no regressions vs baseline, and the cross-app consistency of the shell/nav/login. Produce a go/no-go for the merge+push decision (which remains the user's call).
|
||||
|
||||
---
|
||||
|
||||
## Dependency summary
|
||||
|
||||
- `0.1, 0.2` → `0.3` → `0.4`.
|
||||
- `0.4` blocks `1.1`, `2.1`, `3.1`.
|
||||
- Within Phase 1: `1.1` → {`1.2`, `1.3`, `1.4`, `1.5`} (parallel) → `1.6`.
|
||||
- Within Phase 2: `2.1` → {`2.2`, `2.3`, `2.4`, `2.5`} (parallel) → `2.6`.
|
||||
- Within Phase 3: `3.1` → {`3.2`, `3.3`, `3.5`} (parallel) and `3.1`→`3.4`→`3.6`; all → `3.7`.
|
||||
- `{1.6, 2.6, 3.7}` → `4.1`, `4.2` → `4.3`.
|
||||
- Phases 1/2/3 are independent repos (may run concurrently; listed in risk order).
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-06-03-ui-theme-adoption.md",
|
||||
"tasks": [
|
||||
{"id": 44, "subject": "Task 0.1: NavRailSection persistence key", "status": "pending"},
|
||||
{"id": 45, "subject": "Task 0.2: nav-state.js enhancer + ThemeScripts", "status": "pending"},
|
||||
{"id": 46, "subject": "Task 0.3: Bump 0.2.0 + full suite", "status": "pending", "blockedBy": [44, 45]},
|
||||
{"id": 47, "subject": "Task 0.4: Publish 0.2.0 to Gitea feed", "status": "pending", "blockedBy": [46]},
|
||||
{"id": 48, "subject": "Task 1.1: OtOpcUa NuGet wiring + usings", "status": "pending", "blockedBy": [47]},
|
||||
{"id": 49, "subject": "Task 1.2: OtOpcUa App.razor ThemeHead/ThemeScripts", "status": "pending", "blockedBy": [48]},
|
||||
{"id": 50, "subject": "Task 1.3: OtOpcUa migrate CSS, delete theme.css/fonts/nav-state.js", "status": "pending", "blockedBy": [48]},
|
||||
{"id": 51, "subject": "Task 1.4: OtOpcUa MainLayout to ThemeShell + kit nav", "status": "pending", "blockedBy": [48]},
|
||||
{"id": 52, "subject": "Task 1.5: OtOpcUa delete dead StatusBadge + Login to LoginCard", "status": "pending", "blockedBy": [48]},
|
||||
{"id": 53, "subject": "Task 1.6: OtOpcUa build/test/visual checklist", "status": "pending", "blockedBy": [49, 50, 51, 52]},
|
||||
{"id": 54, "subject": "Task 2.1: ScadaBridge NuGet wiring + usings", "status": "pending", "blockedBy": [47]},
|
||||
{"id": 55, "subject": "Task 2.2: ScadaBridge Host App.razor ThemeHead/ThemeScripts", "status": "pending", "blockedBy": [54]},
|
||||
{"id": 56, "subject": "Task 2.3: ScadaBridge migrate CSS, delete theme.css/fonts/nav-state.js", "status": "pending", "blockedBy": [54]},
|
||||
{"id": 57, "subject": "Task 2.4: ScadaBridge MainLayout/NavMenu to ThemeShell (preserve AuthorizeView/DialogHost/SessionExpiry)", "status": "pending", "blockedBy": [54]},
|
||||
{"id": 58, "subject": "Task 2.5: ScadaBridge Login to LoginCard", "status": "pending", "blockedBy": [54]},
|
||||
{"id": 59, "subject": "Task 2.6: ScadaBridge build/test/visual checklist", "status": "pending", "blockedBy": [55, 56, 57, 58]},
|
||||
{"id": 60, "subject": "Task 3.1: MxGateway NuGet wiring + usings (no central PM)", "status": "pending", "blockedBy": [47]},
|
||||
{"id": 61, "subject": "Task 3.2: MxGateway App.razor ThemeHead/ThemeScripts", "status": "pending", "blockedBy": [60]},
|
||||
{"id": 62, "subject": "Task 3.3: MxGateway migrate CSS, delete theme.css/fonts/nav-state.js", "status": "pending", "blockedBy": [60]},
|
||||
{"id": 63, "subject": "Task 3.4: MxGateway split combined MainLayout to ThemeShell", "status": "pending", "blockedBy": [60]},
|
||||
{"id": 64, "subject": "Task 3.5: MxGateway StatusBadge to StatusPill adapter", "status": "pending", "blockedBy": [60]},
|
||||
{"id": 65, "subject": "Task 3.6: MxGateway net-new Blazor LoginCard page", "status": "pending", "blockedBy": [63]},
|
||||
{"id": 66, "subject": "Task 3.7: MxGateway build/test/login smoke", "status": "pending", "blockedBy": [61, 62, 63, 64, 65]},
|
||||
{"id": 67, "subject": "Task 4.1: Update component docs", "status": "pending", "blockedBy": [53, 59, 66]},
|
||||
{"id": 68, "subject": "Task 4.2: Update memory", "status": "pending", "blockedBy": [53, 59, 66]},
|
||||
{"id": 69, "subject": "Task 4.3: Final integration review", "status": "pending", "blockedBy": [67, 68]}
|
||||
],
|
||||
"lastUpdated": "2026-06-03"
|
||||
}
|
||||
Reference in New Issue
Block a user