chore(admin-web): defer UX-6 (no feed) + complete cross-cutting CC.1-CC.6 (UX-6/CC)

- UX-6 system banners DEFERRED: platform-service (:4003) is unreachable in this
  environment, so there is no real broadcasts/maintenance feed to surface.
  Per the wave's explicit condition, banners are not added against an empty feed.
  Recorded in the waves list + Deferrals table with a follow-up.
- CC.1-CC.6 ticked: suite/build green every wave; dark-mode parity via the bridge;
  zero new color literals; a11y labels on all new controls; charts/palette/motion
  code-split via next/dynamic (chart chunk ~3.8 KB gzip); size:check has no
  bundlesize config in-repo so gzip sizes recorded inline (follow-up logged).
- Add token-bridge guard test (CC.2/CC.3): asserts every --bl-* maps to an admin
  var that flips under .dark and that the bridge contains no raw color literals.

Verify: typecheck+lint+build green (123 routes); vitest 22 files / 183 tests;
format:check no new failures (29 pre-existing); e2e 11 passed / 80 failed
(unchanged vs UX-1 baseline — environmental, no backend).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-05-29 14:24:27 -07:00
parent f12009f49c
commit 263bee6886
2 changed files with 95 additions and 12 deletions

View File

@ -174,23 +174,43 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa
only (not scanned by the public e2e set), per the tracker-web axe/opacity caution. test
**21 files / 170 tests** (+2, happy-dom asserts primitives end visible + render all children);
typecheck+lint+build green; format:check no new failures; e2e unchanged.
- [ ] **UX-6 — System banners (conditional):** if a real broadcasts/maintenance feed is reachable,
add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` in `(dashboard)/layout.tsx`.
`NotificationCenter` only with a real feed; else **defer**.
- [~] **UX-6 — System banners (conditional):** if a real broadcasts/maintenance feed is reachable,
add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` in `(dashboard)/layout.tsx`.
`NotificationCenter` only with a real feed; else **defer**.
**DEFERRED** · No real broadcasts/maintenance feed is reachable in this environment:
`platform-service` (`:4003`) refuses connections (the same backend gap that makes 80/91 e2e
fail), so the admin `/api/maintenance` + `/api/broadcasts` proxies have nothing to surface.
Per the wave's explicit condition (and the run brief), banners are **not** added against an
empty/unreachable feed — adding `BannerStack`/`NotificationCenter` here would be unverifiable
and could render a permanent empty/erroring banner. Follow-up recorded in the Deferrals table.
## Cross-cutting
- [ ] **CC.1** Full suite + build green every wave. · [ ] **CC.2** Dark-mode parity (bridge works in `.dark`).
- [ ] **CC.3** No new color literals. · [ ] **CC.4** No new a11y violations; labels on all controls.
- [ ] **CC.5** Bundle: dynamic `import()` for charts/palette; record gzipped sizes / `size:check`.
- [ ] **CC.6** Final tracker + Deferrals table complete.
- [x] **CC.1** Full suite + build green every wave. — `typecheck`+`lint`+`build` green and `vitest`
passing after each wave; final **22 files / 183 tests**.
- [x] **CC.2** Dark-mode parity (bridge works in `.dark`). — Bridge maps every `--bl-*` to an admin
`--*` var (or `color-mix` of one) that flips between `:root` and `.dark`, so parity is inherited;
guarded by `src/__tests__/token-bridge.test.ts`.
- [x] **CC.3** No new color literals. — All new color values are `var(--*)` tokens (incl. the
`var(--chart-*)` categorical palette); bridge introduces only `var()`/`color-mix` (asserted by
the token-bridge test); grep of every touched file finds zero new hex/oklch/hsl/rgb literals.
- [x] **CC.4** No new a11y violations; labels on all controls. — Charts render `role="img"`+`<title>`
(`ariaLabel`); palette dialog has `ariaLabel="Admin command palette"`; `LoadingSpinner` is
`role="status"`; motion primitives resolve to opacity 1 (no contrast trap). No new unlabeled
controls introduced. (Full `@axe-core` gate needs the backend — deferred; see Deferrals.)
- [x] **CC.5** Bundle: dynamic `import()` for charts/palette; record gzipped sizes. — Charts are
code-split via `next/dynamic` into their own async chunk (**~11.0 KB raw / ~3.8 KB gzip**);
the command-palette dialog and `@bytelyst/motion` are also `next/dynamic`/deferred. `size:check`
(`bundlesize`) has **no config in this repo** (`Config not found`) → recorded gzip sizes here per
the roadmap fallback; adding a `bundlesize` budget config is left as a follow-up.
- [x] **CC.6** Final tracker + Deferrals table complete. — see below.
## Progress tracker
```
Setup : UX-1 ✅
Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ✅ UX-6 ⬜(cond)
Cross : CC.1 ⬜ CC.2 ⬜ CC.3 ⬜ CC.4 ⬜ CC.5 ⬜ CC.6 ⬜
Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ✅ UX-6 ⏭️ deferred (no feed)
Cross : CC.1 ✅ CC.2 ✅ CC.3 ✅ CC.4 ✅ CC.5 ✅ CC.6 ✅
```
## E2e baseline (captured on UX-1 — 2026-05-29)
@ -213,9 +233,11 @@ count must stay ≥ 11 passed / ≤ 80 failed. (A full green run requires the ba
## Deferrals (fill in as encountered)
| Item | Reason (surface/data gate) | Follow-up |
| ------------ | -------------------------- | --------- |
| _(none yet)_ | | |
| Item | Reason (surface/data gate) | Follow-up |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **UX-6 — System banners** | No real broadcasts/maintenance feed: `platform-service` (`:4003`) unreachable in this env, so `/api/maintenance` + `/api/broadcasts` return nothing. | When the backend stack runs, add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` (+`NotificationCenter`) in `(dashboard)/layout.tsx` wired to the live feed, and verify against real data. |
| `@bytelyst/charts` `StackedBar` | Charts 0.1.1 ships only single-series/vertical bars (StackedBar deferred to charts 0.2.x), so the client-logs stacked severity chart was rendered as single bars colored by dominant severity. | Restore a true stacked severity chart once `@bytelyst/charts` 0.2.x lands `StackedBar`. |
| e2e full-green / `@axe-core` gate | 80/91 Playwright specs need the auth backend (`:4003`) which is down here; no `@axe-core` harness exists in `e2e/` yet. | Run the full e2e + add an `@axe-core/playwright` a11y gate once the backend/emulator stack is reachable in CI. |
---

View File

@ -0,0 +1,61 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
/**
* UX-1 / CC.2 / CC.3 guard for the `--bl-*` admin-OKLCH token bridge.
*
* Dark-mode parity is *inherited*: every bridged `--bl-*` token must resolve to
* an admin shadcn `--*` var (or a `color-mix` of one), which already flips
* between `:root` and `.dark`. This test pins that contract so a future edit
* can't silently hardcode a one-mode literal (which would break `.dark` parity
* and violate the zero-new-color-literals rule).
*/
const css = readFileSync(join(__dirname, '..', 'app', 'globals.css'), 'utf8');
// Isolate the UX-1 bridge block.
const bridge = css.slice(css.indexOf('@bytelyst/ui token bridge'));
const REQUIRED_MAPPINGS: Array<[string, string]> = [
['--bl-bg-canvas', 'var(--background)'],
['--bl-surface-card', 'var(--card)'],
['--bl-surface-muted', 'var(--muted)'],
['--bl-text-primary', 'var(--foreground)'],
['--bl-text-secondary', 'var(--muted-foreground)'],
['--bl-border', 'var(--border)'],
['--bl-input', 'var(--input)'],
['--bl-accent', 'var(--primary)'],
['--bl-accent-foreground', 'var(--primary-foreground)'],
['--bl-danger', 'var(--destructive)'],
['--bl-focus-ring', 'var(--ring)'],
];
describe('--bl-* token bridge (dark-mode parity)', () => {
it.each(REQUIRED_MAPPINGS)(
'maps %s to %s (an admin var that flips under .dark)',
(token, target) => {
const re = new RegExp(`${token}\\s*:\\s*${target.replace(/[()]/g, '\\$&')}\\s*;`);
expect(re.test(bridge)).toBe(true);
}
);
it('introduces no raw color literals in the bridge (only var()/color-mix tokens)', () => {
// Strip CSS comments, then look for hex / oklch / hsl / rgb literals.
const code = bridge.replace(/\/\*[\s\S]*?\*\//g, '');
expect(/#[0-9a-fA-F]{3,8}\b/.test(code)).toBe(false);
expect(/\boklch\(/.test(code)).toBe(false);
expect(/\bhsl\(/.test(code)).toBe(false);
expect(/\brgb\(/.test(code)).toBe(false);
});
it('every bridged --bl-* value references an admin var', () => {
// Each `--bl-foo: <value>;` line inside the bridge must contain `var(--`.
const declRe = /(--bl-[a-z0-9-]+)\s*:\s*([^;]+);/g;
let m: RegExpExecArray | null;
const offenders: string[] = [];
while ((m = declRe.exec(bridge))) {
if (!m[2].includes('var(--')) offenders.push(`${m[1]}: ${m[2].trim()}`);
}
expect(offenders).toEqual([]);
});
});