feat(ui): container SlotCard variant + N1 slotStatus unifier#668
Conversation
thinmintdev
left a comment
There was a problem hiding this comment.
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 != nullinterim 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):
-
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 offstatefor lemond) — OR if the exclusion is deliberate, correct the docstring and call out the memory-attribution change explicitly.
-
stateChipClass string overload now colors lemond
warmingamber (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.
42e25de to
35c03d9
Compare
…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>
35c03d9 to
920235f
Compare
Summary
ui/src/dash/slot-status.jswithslotPhase(slot)+slotIndicatorFromPhase().slotIndicator()delegates container slots to the container classifier; lemond slots go through the original code unchanged — all 16slot-indicator.spectests stay green with no modifications.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) readscontainer_status/container_health.InlineSwapPopovershows "· cold restart" badge in the header for container slots and emits an info toast before firing the swap. Lemond hot-swap unchanged.Slotinterface gainsruntime,profile,image,image_status,container_status,container_health.normalizeSlotpasses them through.@media (prefers-reduced-motion: reduce)kills pulse animation on.dot.serving/.warming/.loading. Enable-toggle getsaria-label+role=switch/aria-checkedon the visible track. N3:slot-actionsgetstouch-action:manipulation.slot-card-container-v3.spec.tscovering all container dot states + lemond regression guard + card-level chip assertions.Closes #657
Test plan
slot-indicator.spec.ts— all 16 lemond tests pass unchangedslot-card-container-v3.spec.ts— all 13 new container tests passslots-v3.spec.ts— all 6 existing tests passslot-lifecycle-controls-v3.spec.ts— all 3 passmemory-map-v3.spec.ts— all pass🤖 Generated with Claude Code