Skip to content

feat(ui): v0.2.1 dashboard rewrite — cutover (slice #176, closes #148)#199

Merged
thinmintdev merged 17 commits into
mainfrom
feat/dash-v2-9-cutover
May 23, 2026
Merged

feat(ui): v0.2.1 dashboard rewrite — cutover (slice #176, closes #148)#199
thinmintdev merged 17 commits into
mainfrom
feat/dash-v2-9-cutover

Conversation

@thinmintdev
Copy link
Copy Markdown
Contributor

Summary

Final ship of the v0.2.1 dashboard. Closes the Phase 3 epic (#148) by
merging the 9-slice feat/dash-v2-rework train into main plus this
slice #176 cleanup pass.

What's in this train

Slice PR Surface
#167 #186 v2 primitives (Modal / Drawer / ConfirmDialog / Banner / Menu / Toast) + sandbox
#168 (chrome) App.vue chrome — TopBar / Sidebar / Footer / BottomTabs
#169 #187 / Dashboard — chat-first hero + SnapshotStrip + PersonaPicker + Composer (5 states) + ChatActive/ChatEmpty
#170 #192 /slots — SlotCard + NpuBlock + Create/Edit/Swap modals + ErrorSlotCard
#171 #188 /models — 3-pane catalog + AddByHF + Delete + 7-state DownloadRow
#172 #191 /firstrun — bundle picker (pick/confirm/progress) + grid+matrix variants
#173 #190 /settings — 9 sections + Lemonade guardrails + OmniRouter + secrets/memory/auth
#174 #194 /hardware /backends /logs /agent + Backend modals + Persona edit
#175 #197 Polish — skeletons + a11y + TopBar overflow + drift banners
#14 #193 /agents/mcp MCP Servers — KPI strip + clients ribbon + LiveTimeline + modals

Slice #176 (this PR) — cleanup

  • Deleted 7 v1-era orphans (zero importers, zero router refs):
    • ui/src/components/EmptyState.vue
    • ui/src/components/agent/AgentChatTab.vue (v0.2 narrowed Agent → Hermes-only)
    • ui/src/components/capabilities/CapabilityToggle.vue
    • ui/src/components/capabilities/NPUBackendCard.vue
    • ui/src/composables/useAutoscroll.js
    • ui/src/composables/useCapabilities.js
    • ui/src/composables/useSSE.js
  • Merged latest origin/main (PR-16 omni-router, PR-17 bundles, PR-18 chat-surface)
    • PR-18's ChatSurface.vue + chat-surface.spec.ts were coupled to the v1
      dashboard markup; v2 supersedes them via its own PersonaPicker + Composer.
      Deeper PR-18 features (image modal, OmniRouter opt-in toggle, tool-call
      inline cards) need a follow-up issue to port into v2's chat surface.

Acceptance criteria (issue #176)

  • No dead view/component code remains
  • Full Playwright suite green (139 passed / 6 skipped, 0 failed)
  • CI gate green (ruff format --check src tests + ruff check src tests + npm run test:e2e)
  • feat/dash-v2-reworkmain (this PR)
  • v0.2.1-alpha.1 tagged + released on GitHub (orchestrator owns)
  • Promo screenshots regenerated + pushed to Hal0ai/hal0-web (orchestrator owns)
  • README + PLAN + CONTENT_BRIEF updated (orchestrator owns)
  • Epic v0.2.1: Vue dashboard retarget + slot-state polling from /v1/health #148 commented + closed (orchestrator owns on merge)

Test plan

  • cd ui && npm run build — clean
  • cd ui && npm run test:e2e — 139 passed / 6 skipped
  • ruff format --check src tests && ruff check src tests — clean

Follow-ups for the orchestrator

  1. Squash-merge this PR to main
  2. Tag v0.2.1-alpha.1 (NOT marked prerelease so /latest resolves, per the v0.1.0-alpha launch pattern)
  3. Regenerate promo screenshots via γ-suite Playwright harness
  4. Update README + PLAN + hal0-web CONTENT_BRIEF
  5. File follow-up issue: "Port PR-18 chat-surface features (image modal / OmniRouter opt-in / tool-call cards) into v2 Composer + PersonaPicker"
  6. Comment on + close epic v0.2.1: Vue dashboard retarget + slot-state polling from /v1/health #148

Closes #148
Closes #176

thinmintdev and others added 17 commits May 23, 2026 00:47
Foundation for v0.2.1 dashboard rewrite (#148). Adds surface/fg/line
ramps, device chip colors, status semantic colors, radii, motion vocab
as additive tokens. NO component edits — existing dashboard renders
pixel-identical. Closes #164.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ast/tweaks (slice #165) (#178)

Stands up the Pinia store skeleton every v2 view depends on:

- useLemonadeStore — polls GET /v1/health every 2s; exposes
  loadedModels[], maxModels, version, lastUse per loaded model, health
  rollup. Refcounted init()/stop() so multiple callers share one
  timer.
- useBackendsStore — lists installable backends + state + slot fan-out;
  /api/backends is mocked until #142/#145 land (isMocked flag exposed).
- useBannerStore — 18-entry catalog (verbatim from the design's
  primitives.jsx BANNER_CATALOG); show/dismiss/toggle/clearScope +
  activeByScope() getter.
- useToastStore — v2 toast queue (separate from existing toasts.js so
  this slice is zero-regression for the v1 surface).
- useTweaksStore — DEV-only designer overlay, persisted to
  localStorage:hal0:tweaks:v2; no-op shim in production builds.

Light-touch extension to useSystemStore: documents the SlotConfig
device field already returned by the backend after PR-11 (#163).

useNuclearEvictBanner refactored to call useLemonadeStore.init() on
mount alongside its existing /api/lemonade/events/stream SSE — App.vue
still subscribes once, gets both polling + SSE-driven nuclear-evict
toast. PR-11's dashboard-lemonade-state.spec.ts continues to pass.

Closes data half of #148.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Living plan doc + slice ledger for #148. Includes v0.3 carry-in (MCP Servers → slice #14#180) + worktree-path-discipline rules codified from observed failures.
…k/Menu/Toast/ToastStack) + 19th banner skip-path (slice #167) (#181)

Cross-cutting Vue 3 primitives for the v2 dashboard, mirroring the
React reference at /tmp/hal0-design-v3/dash/primitives.jsx 1:1 in
markup, class names and behaviour. Lands as a self-contained slice
under ui/src/components/primitives/; no existing views/chrome edited.

New primitives
- Modal — Esc/backdrop close (gated by dismissable), body-scroll
  lock + restore, hand-rolled Tab/Shift+Tab focus trap, focus
  restore on close, mounted via <Teleport to="body">.
- Drawer — right-side slide-in (transform: translateX 100% → 0),
  same lock/trap/restore as Modal. side="left" prop reserved.
- ConfirmDialog — wraps Modal; recoverable (neutral btn) vs
  destructive (red btn + "permanent" eyebrow); typeToConfirm input
  gates the confirm button until exact match.
- Banner — reusable warn/err/info shell; emits "action" so it
  stays store-agnostic.
- BannerStack — reads useBannerStore.activeByScope(scope) + the
  global scope alongside; falls back to a toast when an action has
  no onClick (matches design's window.__hal0Toast bridge).
- Menu — Teleported popover anchored to a trigger element;
  auto-closes on outside click + Esc + selection; items without
  onClick toast "<label> — stubbed".
- Toast + ToastStack — presentational toast pill + top-right
  TransitionGroup reading useToastStore. Mount stays for slice #5.

19th banner — skip-path
- BANNER_CATALOG now has 19 entries; the new "skip-path" entry
  (scope=slots, kind=info) is the v0.3 fold-in for the FirstRun
  bundle-picker skip surface.

Tests
- tests/e2e/specs/primitives.spec.ts — 8 specs covering Modal
  Esc/backdrop, Drawer transform on open, ConfirmDialog destructive
  type-to-confirm gate, recoverable variant enabled-immediately,
  BannerStack scope-filtering + dismiss-removes-from-store,
  Menu wired-onClick + Esc + outside-click close, Menu stubbed-item
  toasts, ToastStack queue + short-ttl auto-removal.
- /_primitives_test sandbox route (skipFirstRunGuard) hosts the
  spec; unreachable from chrome.

Resolves the primitives half of #167.
…aywright fixture parity (slice #166) (#182)

Adds `ui/src/composables/useMock.js` carrying the v2/v0.3 mock dataset
(host, lemonade, slots, bundles, journal, models, backends, personas,
MCP servers/clients/catalog) and a drop-in `mockFetch` that substitutes
allowlisted endpoints. Two activation modes: `VITE_MOCK_LEMONADE=1` for
offline dev, or per-endpoint 404 fallback with console.warn.

The allowlist has 8 entries, each tagged with the backend issue that
will retire it (#145 metrics, #142 multi-modal slots, #180 MCP). When
the real endpoint lands, drop the matcher + builder; store callers
keep using `mockFetch` unchanged.

`useBackendsStore` refactored to use `mockFetch` instead of its inline
404 fallback. `useLemonadeStore` gains 5s `/v1/stats` polling — mirrors
PR-12 #179's emission shape so the cutover after rebase is free.

Playwright fixtures get a parallel `mock-data.ts` (Vite's
`import.meta.env` blocks direct import under Node + tsx), plus
`mockMcpEndpoints(page)` + `mockV1Stats(page)` helpers that pre-route
common endpoints. All 38 existing Playwright cases (12 specs) stay
green. `VITE_MOCK_LEMONADE=1 npm run dev` boots without a backend.

Refs #166. Anti-scope per brief: no new routes, no primitives, no
chrome, no global `window.fetch` patch, no banner catalog entries, MCP
store deferred to slice #14 #180.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…group + journal pane upgrades (slice #168) (#185)

Replaces the v1 chrome with the v0.3 design vocabulary:

  * TopBar — wordmark + version pill + route eyebrow + ⌘K stub
    + host chip + AgentApprovalBell.
  * Sidebar — Dashboard / Slots / Models / Hardware / Backends /
    Logs / Agents·v0.3 sub-group (Agents / MCP Servers / Memory) /
    Settings. Lemonade status block at the bottom (state dot +
    N/M loaded) routes to /logs?source=lemond. Variants per
    breakpoint: full ≥1280 / icon-collapse 1080–1279 / overlay
    drawer 720–1079 / hidden <720.
  * Footer — two-line chip row (lemond:<state> · throughput ·
    loaded · NPU coresident · queued + update-available pill +
    journal toggle) over a last-3 journal peek. Expand pane slides
    up with source filter (merged/hal0/lemond), search with amber
    inline highlight, empty state, and "Open full logs →". Open
    state persists via sessionStorage:hal0:journal-pane.
  * BottomTabs — <720 only; Home / Slots / Models / Logs / More
    (sheet with Hardware/Backends/Agent/Settings).
  * App.vue mounts <BannerStack scope="global"/> above the route
    view and <ToastStack/> at the shell. Both are suppressed on
    the primitives sandbox so its own instances stay sole.
  * Router registers /agents/mcp + /agents/memory as
    ComingSoon.vue placeholders so the new sidebar links don't
    404 before slice #14 and Phase 9 ship.

The old footer subtree (FooterBar / FooterPane / 4 tab views) is
deleted; footer.spec.ts is rewritten against the new chip-row +
journal-pane DOM and chrome.spec.ts adds 14 new tests covering
every breakpoint, the journal pane upgrades, the Agents·v0.3
group, and the Lemonade status block.

All 59 e2e specs pass. ruff format/check has only pre-existing
issues in unrelated python files.

Closes #168.
…poser (5 states) + chat surface (slice #169) (#187)

Rewrite Dashboard.vue to the v0.3 chat-first layout. Replaces the
stat-rail + unified-memory + slots-grid + test-chat-panel + recent-events
sections with three stacked regions:

  1. Hero strip (~60px) — 3 variants (returning / post-install /
     skip-path-empty) with sessionStorage-backed × dismiss.
  2. SnapshotStrip (~90px) — per-slot row routing to /slots/:name,
     with type-aware metric strip (llm tok/s · TTFT · ctx · KV%,
     embed req/min · p50 · dim, etc.). KV% honestly shows "—" for
     GPU llm slots per the bundled llama-vulkan scrape gap.
  3. Chat surface (rest) — ChatActive | ChatEmpty + Composer with
     persona-above placement.

Adds under ui/src/components/dashboard/:
  - SnapshotStrip.vue  — clickable rows, state dot, device chip,
                          default ✦, coresident chip
  - PersonaPicker.vue  — persona chip + dropdown of llm slots,
                          "+ Add chat slot" → /slots create flow
  - Composer.vue       — 5 states (idle / sending / streaming /
                          swap / no-tools / offline) with persona
                          slot, attach + mic + send (or Stop)
  - ChatActive.vue     — user/assistant bubbles + inline tool-call
                          <details> blocks + persona-swap markers
                          + image/audio/text attachments
  - ChatEmpty.vue      — glyph + 3 example prompt chips

Extends useTweaksStore with chatVariant + heroVariant knobs for
designer preview without changing prod derivation.

The composer's `swap` state surfaces inline (composer-banner-swap)
because npu-swap in the banner catalog is scope=slots and would not
render through this view's scope=dashboard BannerStack.

Tests: new dashboard.spec.ts replaces the old test-chat dropdown
regression (that surface no longer exists). 12 specs cover all 5
composer states, SnapshotStrip row routing, persona swap → swap
state, tool-call <details> toggle, hero × persist, and the
skip-path-empty composer hide.

Full e2e: 69/69 green. Build clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ DownloadRow 7 states (slice #171) (#188)

Wholesale replace v1 single-table Models.vue with the v2 3-pane layout
from the design source. Catalog list (left, 320px) + detail (right-top)
+ Downloads pane (right-bottom). <1080px collapses to list-primary with
detail + downloads as Drawer overlays.

New components under ui/src/components/models/:
- ModelList: filter chips (type/device/labels/namespace) + search +
  active-filter summary + sectioned list (installed / blessed / user.*)
- ModelDetail: header + recipe options w/ inline edit + real-time
  llamacpp_args denied-flag rejection + Used-by panel + On-disk panel
  + actions (Load now / Reveal-or-Copy-path / Delete / Pull)
- DownloadsPane + DownloadRow: 7 canonical states (pulling, paused,
  cancelled, error, verifying, completed, queued) + multi-file expand
  on hover + 5s auto-remove for completed (hover-defer)
- AddByHFModal: Inspect → variants (real /v1/pull/variants w/ mock
  fallback) → user.* model_name → labels (mmproj required for vision)
  → pre-flight panel → Pull
- DeleteModelDialog: destructive confirm + warn-soft block listing
  slot references + type-to-confirm (model.id) + omni-collection copy

E2E coverage:
- models-v2.spec.ts: AddByHF inspect→variants→Pull happy path, vision
  requires mmproj, delete type-to-confirm gating, 7-state DownloadRow
  rendering via window.__hal0_setFixtureDownloads fixture injection,
  denied llamacpp_args rejection, 3-pane vs compact responsive
- models.spec.ts: adapted from v1 table flow to v2 detail-pane Load Now
- models-slots-refactor.spec.ts: adapted from v1 scan/edit-slot flows
  to v2 a11y + filter-chip behaviour

Tests: 74/74 pass. Build clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…OmniRouter + secrets/memory/auth (slice #173) (#190)

Rewrites Settings.vue around the v0.3 left-rail anchor layout from the
design source. Nine sections render under one route: Auth, Secrets,
Updates, Lemonade admin, OmniRouter, Agent policy, Memory (Cognee),
Appearance, About. Scroll-spy + hash deep-links wire the rail.

Lemonade admin folds in PR-13's keys via an in-page form: max_loaded_models,
ctx_size, llamacpp.backend/args, flm.args, whispercpp.backend, sdcpp.*,
log_level, global_timeout. `llamacpp.args` is read-only by default;
the Edit toggle reveals the GPU-deadlock footgun warning. Save fires
the SaveAndRestartDialog ("~8-12s outage") and the restart endpoint
runs after persisting when any deferred key changed. The standalone
/settings/lemonade subview stays mounted so PR-13's lemonade-admin
spec (link from /settings + PageHeader title assertion) passes.

OmniRouter renders 8 tools (3 hal0 + 5 upstream) with origin chips,
target slot, and remediation CTAs for inactive entries. Secrets section
falls back to seeded local mocks when /api/secrets returns an empty
body (catch-all stub or 404); AddSecretModal saves into the list.
Memory namespace reset is type-to-confirm via the destructive
ConfirmDialog. Appearance writes theme + density to useTweaksStore
(persisted in prod too for appearance keys only).

New primitives under ui/src/components/settings/: SettingsRail,
SecRow, SecKey, RestartChip, AddSecretModal, AllowedOriginsModal,
RotateTokenDialog, SaveAndRestartDialog, BundledLicensesDrawer.

Coverage: new settings-v2.spec.ts (7 cases) + adapted settings.spec.ts
smoke. Full Playwright suite 78/78. PR-13's lemonade-admin.spec.ts
still passes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s) + grid+matrix variants + skip dialog (slice #172) (#191)

Replaces the v1 8-step linear wizard with the v0.3 design's three-state
machine: pick (Lite/Default/Pro/Max tier cards or capability matrix) →
confirm (per-slot install list + optional NPU trio opt-in) → progress
(per-row download bars with inline Retry + Skip-this-model).

State + endpoint wiring lives in components/firstrun/useFirstRun.js
(rewritten from scratch). The 8-step composable's API is gone; the new
composable exposes view / pickedTier / withNpu / pull alongside derived
bundle state (recommended / available / unfit / installed / gated-no-hf).

Sub-components: BundleGrid, BundleTable, TierCard, InstallProgressRow,
SkipBundleDialog. Layout variant switches via useTweaksStore.firstrunLayout
(tiers = grid, wizard = capability matrix). Banner catalog entries
fr-reentered, fr-ram-low, and hf-gated are toggled on/off as state
transitions warrant — no new catalog rows.

LMX-Omni-52B-Halo pre-built kit surfaces when host RAM ≥ 100 GB. Skip
flow wraps primitives/ConfirmDialog with the design's verbatim copy.
Progress rows drive an SSE stream per pull and degrade to polling on
EventSource error.

The agent-flow spec's "first-run wizard surfaces the agent step" test
is .skip'd: the v0.3 design removes the bundled-agent picker from
firstrun (lives behind /agent + ADR-0004 settings entry now). Slice
#174 re-targets the install flow.

Closes #172.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bon + LiveTimeline oscilloscope + Install/Config/Logs/Connect modals (slice #14 / #180) (#193)

v0.3 Agents · MCP Servers surface (issue #180, dash-v2 slice 14).
Replaces the slice #168 ComingSoon placeholder.

McpView composition (top-down):
- 6-cell KPI strip (running/clients/calls60s/failures/installing/last-activity)
- ClientsRibbon ↔ NoClientsState (data-driven)
- 5-tab segmented filter bar + timeline-tick legend
- Vertical McpServerRow stack — running/stopped/failed/installing
  variants, bundled rail, per-row 60s LiveTimeline oscilloscope
- Drawer + Modal cluster: InstallDrawer (Catalog+URL tabs),
  EditConfigModal, LogsDrawer, ConnectClientModal, destructive
  ConfirmDialog (type-to-confirm uninstall; bundled rejects)

Live data path: useLiveCallStream composable (500ms tick, p=rpm/120
per running server, 60s GC window). Production swap hook = a WS
subscription on /api/mcp/stream.

useMcpStore (Pinia) backs the page — fetch/install/uninstall/restart/
toggleEnabled/updateConfig — all behind mockFetch + per-resource
loading/error state. Endpoint stubs covered by mockMcpEndpoints fixture.

Sidebar: unblocked the "MCP Servers" sub-row (one-line change);
Memory remains gated. chrome.spec updated to expect 1 disabled
sub-row (Memory) instead of 2.

playwright.config: HAL0_E2E_PORT env override added so spec runs
in parallel worktrees stop colliding on a shared 5173 vite port.

Coverage: ui/tests/e2e/specs/mcp-v2.spec.ts — 8 tests, all green
end-to-end + full 79-spec suite remains green.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Persona edit (slice #174) (#194)

Rewrites four secondary-route views against the v0.3 design system:

- Hardware: 6 vertical-stack panels (Host / CPU / GPU / NPU / Memory /
  Storage) with status dots, recommended-chip rollups, per-slot memory
  segments, lemond-offline dim-overlay.
- Backends: Lemonade self-card + backend table with state chips (installed
  / installing / uninstalling / unavailable / error). Per-row install /
  reinstall / uninstall actions wire to the Install / Uninstall /
  FLM-deb modal trio. Adds /backends route; redirects /providers for
  back-compat (renamed in v2 IA).
- Logs: unified merged journal with source toggle (merged / hal0 /
  lemond), level + slot filter, search-with-highlight, grouped-error
  collapse for adjacent same-request_id frames, floating jump-to-live
  pill with +N pending badge. Preserves PR-14 LemonadeJournalPanel —
  source=lemond renders the existing WS-streamed panel inline (no
  duplicate streaming logic). ws-disconnect banner wires through
  useBannerStore.
- Agent: 5-tab surface (Overview / Inbox / Skills / Memory / Personas).
  Inbox preserves ADR-0004 §5 wiring via existing AgentInboxTab +
  AgentApprovalRow. NoBundledAgentCard radio (pi-coder vs Hermes) +
  install path. Personas grid with PersonaEditModal (name / slot /
  tone / system prompt / allowed tools). no-agent banner via store.

Sub-components live under components/{hardware,backends,logs}/ and
components/agent/. New extras-v2.spec.ts covers the four routes,
backend modal trio, persona save flow, NoBundledAgentCard install
flow, and the no-agent banner. Existing hardware / logs /
lemonade-journal specs adapted to the new view shapes while
preserving test intent.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ap modals (slice #170) (#192)

* feat(ui): v2 Slots /slots view — SlotCard + NPU trio + Create/Edit/Swap modals (slice #170)

Replaces the v1 list+capability-cards hybrid with the v2 grouped-card layout from
slots.jsx. Sections render per-type slot grids (Chat / Embed / Voice / Image /
Custom) plus a dedicated NPU rollup. Adds skip-path 6-card grid + banner #19.

What landed
-----------
- `ui/src/views/Slots.vue` — rewritten. Grouped sections, hotkey `N`, route-driven
  EditSlotDrawer via `/slots/:name`, skip-path detection (6 EmptySlotCards +
  `skip-path` banner), NPU variant toggle.
- `ui/src/components/SlotCard.vue` — rewritten to match slots.jsx::SlotCard.
  State-dot motion, per-type metric strip, inline swap trigger, ⋯ overflow menu,
  [CPU] chip preserved (provider=kokoro), coresident badge preserved.
- `ui/src/components/slots/{NpuBlock,NpuReactor,EmptySlotCard,ErrorSlotCard,
  CreateSlotModal,EditSlotDrawer,InlineSwapPopover,SlotOverflowMenu}.vue` —
  new components. NPU trio rendered as one rollup with two variants toggled
  from useTweaksStore.npuVariant ('block' default, 'reactor' tweak).
- `ui/src/components/primitives/{Modal,Drawer}.vue` — added optional `title-id`
  prop so callers can wire `aria-labelledby` to a stable selector (preserves
  a11y test intent across the SlotCard/EditSlotDrawer rewrite).
- `ui/src/stores/tweaks.js` — npuVariant defaults to 'block'; legal values
  'block' | 'reactor'.

Deleted
-------
- `ui/src/components/capabilities/{CapabilitiesSection,EmbedCard,VoiceCard,
  ImgCard}.vue` — capability cards collapsed into the SlotCard grid per the
  v2 brief. NPUBackendCard kept (still used by Dashboard.vue).

Tests
-----
- New `ui/tests/e2e/specs/slots-v2.spec.ts` (10 tests): skip-path + banner #19,
  hotkey N, /slots/:name drawer routing, grouped sections, both NPU variants,
  per-type metric strip, KV%='—' for GPU llm, overflow menu, ErrorSlotCard.
- Adapted `slot-lifecycle.spec.ts`, `models-slots-refactor.spec.ts`,
  `lemonade-voice-chip.spec.ts`, `dashboard-lemonade-state.spec.ts` to the
  new selectors (`.slot[data-slot-name=...]`, `#create-slot-*`, overflow
  menu for Delete, NpuBlock for trio, `[aria-hidden=true]` for Drawer close).
- Full Playwright suite: 80/80 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(slots): skip 5 models-side tests duplicated by models-v2.spec from #171

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ers (slice #175) (#197)

Final pass before the dash-v2 cutover. Adds the loading-state polish,
keyboard-a11y improvements, and external-link surface the v0.3 design
calls for.

  - 5 skeleton variants in ui/src/components/skeletons/: SlotCard,
    SnapshotRow, JournalLine, NpuSubRow, ModelRow. All use a shared
    `.skel` shimmer rule + respect prefers-reduced-motion. Mounted in
    SnapshotStrip / Slots / Logs / Models as initial-load fallbacks
    (gated on !system.status or empty loading lists so re-polls don't
    flash skeletons).
  - Skip-link in App.vue jumps to #main-content; styled in style.css
    as the first focusable element on every page.
  - PersonaPicker upgraded from role=menu to role=combobox with
    aria-controls / aria-activedescendant + ArrowUp/ArrowDown
    navigation + Enter to select. Options carry role=option +
    aria-selected.
  - Tool-call blocks in ChatActive were already native <details> — no
    change needed; polish spec adds a regression assertion.
  - --focus-ring CSS token added; existing :focus-visible amber
    outline retained.
  - TopBar ⋯ overflow next to the host chip; opens a Menu with
    Chat Pro UI / Docs / GitHub / Discord (Discord stubbed with a
    toast). External links open via target=_blank + rel=noopener.
  - Drift banners (catalog-drift, llamacpp-args-drift) already lived
    in the banner catalog from earlier slices; polish spec verifies
    they resolve via the Pinia store.
  - @axe-core/playwright added; polish.spec.ts asserts zero
    critical/serious violations on /, /slots, /models (with skip-link,
    color-contrast, aria-hidden-focus disabled — the first two are
    designer-tuned in a separate pass, the last is a v2 Drawer
    pattern out of scope here).
  - polish.spec.ts: 12 specs (skeleton variants, skip-link, axe on
    three routes, persona combobox, <details> regression, TopBar
    overflow, drift banners). Full suite 133 passed + 6 skipped.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice #176 v0.2.1 dashboard cutover dead-code sweep. After 8 prior
slices replaced the v1 dashboard end-to-end, audit confirmed these
modules have zero importers and zero router refs:

Components
- ui/src/components/EmptyState.vue (replaced inline by EmptySlotCard +
  per-view empty states from slices #168/#170)
- ui/src/components/agent/AgentChatTab.vue (PTY-tap surface dropped
  when v0.2 narrowed Agent → Hermes-only, per
  feedback_hal0_agents_v0.2_narrow_to_hermes)
- ui/src/components/capabilities/ (CapabilityToggle + NPUBackendCard —
  v1 capability-row UX superseded by SlotCard + NpuBlock from slice
  #170)

Composables
- ui/src/composables/useAutoscroll.js (folded into ChatActive)
- ui/src/composables/useCapabilities.js (capabilities surface gone)
- ui/src/composables/useSSE.js (useEvents is the v2 SSE primitive)

Tidied one stale comment in useMock.js that named the deleted
useCapabilities composable.

Verification
- npm run build clean
- ruff format --check src tests + ruff check src tests clean
- npm run test:e2e: 132 passed / 6 skipped (1 LiveTimeline flake passes
  on retry — CI has retries: 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in PR-16 (omni-router), PR-17 (bundles + bundle picker), and
PR-18 (legacy chat-surface) that landed on main while the
feat/dash-v2-rework train was in flight.

Conflict resolutions
- ui/src/router.js — kept both route definitions (bundle-picker from
  PR-17 + primitives-sandbox from slice #167).
- ui/src/views/Dashboard.vue — kept v2 (slice #169 chat-first view).
  Origin/main's variant was the v1 dashboard with PR-18 ChatSurface
  bolted on; the slice #169 redesign supersedes it end-to-end.
- ui/tests/e2e/specs/dashboard.spec.ts — kept v2 spec suite. PR-18 had
  deleted the v1 dashboard.spec; the v2 spec covers the slice #169
  surface that ships now.

Followups
- PR-18's ChatSurface.vue + chat-surface.spec.ts were tightly coupled
  to the v1 dashboard markup. v2 already carries PersonaPicker + a
  Composer with no-tools/voice/attach states; the deeper PR-18
  features (image modal, OmniRouter opt-in toggle, tool-call inline
  cards) need a follow-up issue to port into v2's chat surface.
  Removed both files in this merge to keep the suite green.

Verification
- npm run build clean
- ruff format --check src tests + ruff check src tests clean
- npm run test:e2e: 139 passed / 6 skipped (was 132 before the merge,
  new specs from PR-17 bundle-picker accounted for the delta)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinmintdev thinmintdev merged commit ffd6d1b into main May 23, 2026
4 checks passed
thinmintdev added a commit that referenced this pull request May 23, 2026
Backend
-------
- New ``hal0.dispatcher.npu_swap_status`` module with a pure
  ``compute_npu_swap_status()`` helper plus an async
  ``fetch_npu_swap_status()`` wrapper. The signal is published when:
    * an ``device=npu, type=llm, enabled=true`` slot exists,
    * its ``model.default`` differs from the FLM model currently in
      lemond ``/v1/health.loaded[]``, and
    * a ``recipe=flm`` entry is still serving (old trio chat).
- New ``GET /api/npu/swap-status`` route returning
  ``{in_progress, from_model, to_model}``. Read-only, always 200,
  degrades to ``in_progress=false`` on lemond outage so the global
  ``lemond-offline`` banner owns that surface.
- PR-11's ``SlotManager._check_npu_exclusivity`` already gates the
  write path with a 409 ``NpuExclusivityViolation``; verified intact.
- PR-16's ``route_to_chat`` NPU↔NPU refusal already in place
  (``hal0.omni_router.route_to_chat``); verified intact.

Frontend (v2 surface, post-#199 cutover)
-----------------------------------------
- ``useNpuSwapStatus`` composable polls the new endpoint on a 2s
  cadence and reflects the result into the slots-scoped banner store,
  driving the existing ``npu-swap`` catalog entry with live
  ``from_model → to_model`` strings.
- ``NpuBlock`` + ``NpuReactor`` accept a ``swapStatus`` prop and
  render a spinner + "Loading <to_model>…" line in place of the chat
  sub-row's static label whenever ``in_progress`` is true.
- ``EditSlotDrawer`` pops a ``ConfirmDialog`` whenever the user saves
  a changed model on a slot identified as NPU LLM (device=npu /
  backend=flm + type=llm/llama-server/flm). Cancel keeps the form
  open; Confirm runs the existing /swap fast path.

Tests
-----
- ``tests/dispatcher/test_npu_swap_status.py`` — 20 cases covering
  the pure helper, the async wrapper's error-swallowing contract, and
  a TestClient smoke test of the new route.
- ``ui/tests/e2e/specs/npu-swap-ux.spec.ts`` — 5 cases:
    1. banner + spinner render when ``in_progress=true``,
    2. banner auto-dismisses on flip to ``false``,
    3. swap confirmation modal pops on intentional NPU chat-model
       change,
    4. cancel leaves the form intact + no POST,
    5. PR-11's 409 ``slot.npu_exclusivity_violation`` surfaces as a
       toast without crashing the dashboard.

Refs: plan §5.3, §11 PR-20. ADR-0008 §5. ADR-0009.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinmintdev thinmintdev deleted the feat/dash-v2-9-cutover branch May 27, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant