Skip to content

feat(ui): container SlotCard variant + N1 slotStatus unifier#668

Merged
thinmintdev merged 3 commits into
mainfrom
issue-657-card
Jun 8, 2026
Merged

feat(ui): container SlotCard variant + N1 slotStatus unifier#668
thinmintdev merged 3 commits into
mainfrom
issue-657-card

Conversation

@thinmintdev

Copy link
Copy Markdown
Contributor

Summary

  • N1 (status vocabulary unifier): New ui/src/dash/slot-status.js with slotPhase(slot) + slotIndicatorFromPhase(). slotIndicator() delegates container slots to the container classifier; lemond slots go through the original code unchanged — all 16 slot-indicator.spec tests stay green with no modifications.
  • Container card branch: runtime==="container" slots replace the device chip + backend-mismatch block with an IMAGE-TAG chip (last path segment of image ref, full ref on hover). A "container" runtime micro-tag (N5) distinguishes cold-swap behavior. Phase logic (button states) reads container_status/container_health.
  • N2 cold-swap confirm: InlineSwapPopover shows "· cold restart" badge in the header for container slots and emits an info toast before firing the swap. Lemond hot-swap unchanged.
  • useSlots.ts: Slot interface gains runtime, profile, image, image_status, container_status, container_health. normalizeSlot passes them through.
  • A11y: @media (prefers-reduced-motion: reduce) kills pulse animation on .dot.serving/.warming/.loading. Enable-toggle gets aria-label + role=switch/aria-checked on the visible track. N3: slot-actions gets touch-action:manipulation.
  • Playwright: 13-test slot-card-container-v3.spec.ts covering all container dot states + lemond regression guard + card-level chip assertions.

Closes #657

Test plan

  • slot-indicator.spec.ts — all 16 lemond tests pass unchanged
  • slot-card-container-v3.spec.ts — all 13 new container tests pass
  • slots-v3.spec.ts — all 6 existing tests pass
  • slot-lifecycle-controls-v3.spec.ts — all 3 pass
  • memory-map-v3.spec.ts — all pass
  • Clean Vite build (130 modules, 9.1s, no errors)

🤖 Generated with Claude Code

@thinmintdev thinmintdev left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES (2 numbered) — not merge-ready

The architecture is sound and the container variant is approve-quality, but the shared-helper refactor breaks lemond parity in two places (AC #2: "lemond slots visually unchanged"). CI is green but does NOT exercise either regression (see note), so green is not parity-proof here.

WHAT IS RIGHT (dont reopen):

  • slotIndicator() lemond path is byte-identical: the only change is a top guard (runtime==="container" || container_status!=null) delegating container slots to slotIndicatorFromPhase; lemond slots fall through to the unchanged original. Same conservative pattern in the SlotCard isContainer phase branch (lemond else = verbatim original) and stateChipClass object-path (null sentinel → original).
  • Container detection via container_status != null interim fallback is sound — #656 always emits container_status even before as_dict() serialises runtime/image/profile (#658). Graceful degradation when image/profile absent ("no image" / "profile:") is correct.
  • Container dot states (running+health→ready/serving, starting/pulling→warming, crashed→error, stopped/!enabled→offline), N2 cold-swap confirm toast + "· cold restart" badge, N5 runtime micro-tag, a11y (prefers-reduced-motion kills pulse, toggle aria-label, touch-action:manipulation) all correct.

CHANGES (blocking):

  1. isSlotLive diverges from the old LIVE_STATES for lemond slots (memory-map attribution). slot-status.js docstring claims isLive has "same semantics as old LIVE_STATES" — but it does not. Old: liveSlots = state ∈ {ready, serving, idle, warming}. New _lemondPhase().isLive:

    • warming → was LIVE, now isLive=false (→ "starting" branch)
    • idle → was LIVE, now isLive=false (→ "idle" branch returns isLive:false)
    • disabled slot with state=ready → was LIVE (old never checked enabled), now early-returns {stopped, isLive:false}
    • reverse flip: state=offline + lemonade_state=loaded → now isLive=true (old keyed only off state)
      Net: idle/warming/disabled-resident lemond slots silently drop out of the memory map; some offline-but-loaded slots newly appear. This is a regression vs the authors own stated equivalence. FIX: restore equivalence (treat idle+warming as live, ignore enabled, key off state for lemond) — OR if the exclusion is deliberate, correct the docstring and call out the memory-attribution change explicitly.
  2. stateChipClass string overload now colors lemond warming amber (was grey). The string branch added "pulling","warming" to the warn set and "crashed" to err. The sole external call site (slot-modals.jsx:589, EditSlotDrawer state ReadOnlyStrip) passes slot.state as a STRING → hits this branch. A lemond slot in state="warming" (a real lemond state) now renders a "chip warn" (amber) where the old code returned the default grey "chip". Visible lemond change. FIX: keep the string branchs lemond-relevant states classified as before (dont add warming/pulling to warn for the string path), or route that strip through the slot-object overload so only container slots get the new vocab.

CI NOTE: γ-suite + ui are green, but neither regression is covered — memory-map-v3.spec uses only ready/starting slots (no idle/warming lemond slot anywhere in mock-data), and no spec asserts the state-strip chip color for a warming lemond slot. So green CI does not verify lemond parity here. (python jobs are irrelevant — no .py changed.) Worth adding a memory-map test with an idle + a warming lemond slot to pin LIVE_STATES equivalence going forward.

Once #1 + #2 are addressed this is merge-ready — everything else is solid. Unblocks #658/#659/#660.

thinmintdev and others added 3 commits June 8, 2026 07:30
…657)

N1 (highest leverage): extract slot-status.js with slotPhase(slot) → unified
phase/isLive/isCold for both runtimes. slotIndicator() delegates container
slots to slotIndicatorFromPhase() from the new module; lemond slots continue
through the original classifier unchanged so all 16 slot-indicator.spec tests
remain green. stateChipClass() extended to handle container phases (crashed→err,
starting→warn, running+healthy→ok).

SlotCard container branch: slots with runtime==="container" replace the device
chip + backend-mismatch block with an IMAGE-TAG chip (last path segment of the
image ref, full ref on hover). A "container" runtime micro-tag (N5) makes the
cold-swap behavior legible. Phase logic (off/running/transitional buttons) reads
container_status/container_health instead of lemonade_state.

InlineSwapPopover N2: container slots show "· cold restart" in the popover
header and emit an info toast ("Restarting <slot> to load <model> ~Ns") before
firing the swap. Lemond hot-swap path unchanged.

useSlots.ts: Slot interface gains runtime, profile, image, image_status,
container_status, container_health fields. normalizeSlot passes them through.

A11y: @media (prefers-reduced-motion: reduce) kills pulse animation on
.dot.serving/.warming/.loading. enable-toggle gains aria-label on the hidden
input + role=switch/aria-checked on the visible track. N3: slot-actions div
gets touch-action:manipulation.

Playwright: 13-test slot-card-container-v3.spec covering all container dot
states (running→stale, serving→serving, starting→warming, pulled→warming,
crashed→error, stopped→offline, disabled→off) + lemond regression guard +
card-level image-chip / runtime-tag checks via slotIndicator + HAL0_DATA slot
assertions.

Build: clean (✓ 130 modules, 9.1s). All 45 related Playwright specs pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tainer detection + a11y

- memory-map: replace static LIVE_STATES.has() with isSlotLive() from
  slot-status.js — container slots (running+healthy) now correctly show
  as live in the memory bar without needing a lemond state string
- slot-modals: import stateChipClassForSlot, delegate container chip
  coloring through slotPhase() instead of an inline re-implementation
- slot-status: extract _isContainer() helper that detects container runtime
  via `runtime="container"` OR `container_status != null` (backend fallback
  until #658 wires runtime field into as_dict serialisation); apply to
  slotPhase/slotIndicatorFromPhase/stateChipClassForSlot/isSlotLive
- slots: isContainer detection uses same dual signal; slotIndicator()
  container check matches; aria-hidden span loses role=switch (was
  invisible to AT anyway — real semantics are on the hidden checkbox)
- image chip: degrade tooltip documents backend gap (#658)

All 139 Playwright specs pass; build ✓ 130 modules.

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

Two lemond-parity regressions from the N1 slot-status unification, missed by
CI (no idle/warming lemond slots in the mocks):

FIX 1 — isSlotLive memory attribution diverged from legacy LIVE_STATES:
- memory-map used isSlotLive(), which routed through slotPhase().isLive —
  that folds in enabled + lemonade_state, so lemond warming + idle slots
  dropped out of memory attribution, a disabled-but-resident state=ready
  slot dropped out, and state=offline+lemonade_state=loaded wrongly became
  live. None matched the old `LIVE_STATES.has(slot.state)` truth.
- isSlotLive() now matches legacy EXACTLY for lemond: live iff
  state ∈ {ready, serving, idle, warming}, ignoring enabled/lemonade_state.
  Container slots keep their own rule (running + healthy). slotPhase().isLive
  is left as-is for the dot vocabulary; docstring now flags the divergence.

FIX 2 — stateChipClass string path recolored lemond:
- The string overload had gained warming/pulling in the warn set, turning
  lemond state="warming" amber at the EditSlotDrawer state strip (was grey).
- Restored byte-identical to origin/main: warn={starting,loading,pending,
  stopping}, ok includes running, err drops crashed. Container chips route
  through the slot-OBJECT overload only.

TEST — slot-live-equivalence-v3.spec.ts pins isSlotLive(lemond) ===
LIVE_STATES.has(state) across all state strings, with explicit
regression cases for warming + idle, plus the container running+healthy rule.

Build ✓ 130 modules; 70 specs green (16 new equivalence cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thinmintdev thinmintdev merged commit 435f7e7 into main Jun 8, 2026
4 checks passed
@thinmintdev thinmintdev deleted the issue-657-card branch June 8, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard: slot-card container variant (read-only)

1 participant