spec/device-card-states.md — Water heater card decision map (DRAFT 3)
Status: DRAFT 3 (2026-04-28) — incorporates DB-schema review + decisions from session. Locked enough to drive next round (rendering scenarios as state-cards in
states.html), but several items are flagged ASSUMPTION pending API/product confirmation.
What this is
A single source of truth for the water heater card (the UI element that represents a YG device on a water heater) across every valid user/device/supply state. Today it's inconsistent — overview.html says "measuring" before any supply point exists; dashboard.html step 5 echoes YG7A2K; step 7 says "measuring". This matrix locks down what should appear in each scenario so the flow pages can stop disagreeing.
Reframing (user-clarified 2026-04-28): there is no separate "device card". The device (YG hardware) appears as a status badge + state info on the water heater's card — they're the same UI element. Where this doc says "card", it means the heater card; the device shows up as the badge.
Naming conventions used below:
- Install case — A (unpaired), B (3rd-party install), C (self-install). See
ROADMAP.md. - Auth state —
anon/loggedIn(=yg.flow.emailset in browser; backed by aUserrow server-side). - Supply state —
noSupply/supplySet(= an activeWaterHeater → SupplyPointchain on the server, plusyg.flow.hasSupplymirror in browser). - Runtime state —
paired/boost-on/measuring/paused/offline/error. Derived fromTelemetrySnapshot(relay_output, recency oftimestamp,relay_load_attached).
The 1:1 device↔water-heater relation (per DeviceWaterHeater time-bounded link) means the card is about the heater; the device is its monitoring hardware. Default heater name: Water heater / Bojler, with numeric suffix (Water heater 2) when N≥2 heaters are under the same supply point. WaterHeater has no dedicated name field today — likely the user-set name lives in WaterHeater.note (TextField) or a future field. Confirm with API team.
0. Device lifecycle — before the website
The YG device has its own life before the user types its code on boiler.ztejna.cz. The website only sees a device once it's reached "active in YG cloud". Earlier states happen in the device's own mobile app + on the hardware itself.
┌─────────┐ ┌───────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ ┌─────────────────┐
│ D0 │ → │ D1 │ → │ D2 │ → │ user types code on │ → │ S2 / S4 / ... │
│ in box │ │ powered, in │ │ own-app done; │ │ boiler.ztejna.cz │ │ (this matrix │
│ │ │ own-app Wi-Fi │ │ device active │ │ (or scans QR, or │ │ takes over) │
│ │ │ setup │ │ in YG cloud, has │ │ receives email link │ │ │
│ │ │ │ │ code on its label │ │ for Case B) │ │ │
└─────────┘ └───────────────────┘ └───────────────────────┘ └───────────────────────┘ └─────────────────┘
↑ ↑ ↑
manufacturing_status = TBD manufacturing_status = TBD manufacturing_status = TBD
("not yet provisioned" — confirm) ("provisioned, not adopted") ("adopted / online")
The website assumes D2. The actual Device.manufacturing_status enum (PositiveSmallIntegerField per the schema) holds the real integer values; we use the D0/D1/D2 product labels here so the API team can map them to enum integers when we ask. D0/D1/D2 → enum int = TBD; ask API team.
What happens when a user enters a code for a device in D0/D1?
- The YG cloud doesn't recognize the code as adopted yet →
code.htmlshould show a "code valid — finish device setup in the YG app first" hint (better than generic "code not recognized"). The exact UX is out of this doc; flagged in open questions.
For offline (S8) on the website: pinpointing whether the device dropped from D2 back toward D0/D1 (unplugged? Wi-Fi changed? hardware failure?) is the heater-detail page diagnostic story. The badge stays "offline · last seen X"; the detail page guides the user through a reverse-walk.
Shelly devices follow a parallel lifecycle through Shelly's own cloud + Shelly's mobile app. By the time a user pastes a Shelly code on code.html, they've already done Shelly-app setup themselves. Pre-states are not our website's problem.
1. Decision tree (top-down walk)
ROOT — User arrives at a YG-aware page
│
├─ entered a code (YG or Shelly)?
│ ├─ no → /storyboard/code.html (entry; not a card scenario)
│ ├─ Shelly code → Shelly path (parallel; see "Shelly differences" below)
│ └─ YG code → Device exists in YG database?
│ │
│ ├─ NO → leaf S0 "code valid but no Device row" (truly unknown code; code.html error path)
│ │
│ ├─ YES, Device exists, no active DeviceWaterHeater row (Case A — unpaired)
│ │ └── leaf S0u "unpaired — Device exists but not on a water heater yet"
│ │ (Case A — render card-shaped error with code echo + "unpaired" badge + helper text)
│ │
│ ├─ YES, with supply already set up by installer (Case B) → claim flow
│ │ ├── arrived via email link, not yet claimed
│ │ │ └── leaf S1 "claim-pending"
│ │ │ (anon, supplySet, measuring — but card is locked behind claim)
│ │ │
│ │ └── claimed (set password, accepted UserOrganization invite) → loggedIn + supplySet
│ │ └── leaf S6 "measuring, fully set up" (collapse with C-completed)
│ │
│ └─ YES, no supply yet (Case C self-install) → user options
│ │
│ ├── stay anonymous
│ │ │
│ │ ├── do nothing yet
│ │ │ └── leaf S2 "anon, paired, no supply"
│ │ │ (overview.html state — currently mis-rendered as "measuring", BUG)
│ │ │
│ │ └── click "Turn on for 3 days" (anonymous device action)
│ │ └── leaf S3 "anon boost-on" (3-day relay action; no signup required)
│ │
│ └── sign up + verify → loggedIn, no supply yet
│ │
│ ├── before running setup wizard
│ │ └── leaf S4 "loggedIn, paired, no supply"
│ │ (dashboard.html step 5 — empty branch with paired device card)
│ │
│ └── completed setup wizard → loggedIn + supplySet
│ │
│ ├── normal operation → leaf S6 "measuring"
│ ├── user turned heater off → leaf S7 "paused"
│ └── device lost signal → leaf S8 "offline"
Reachable leaves: 9 (S0 split into S0/S0u; S5 skipped to leave room for future C-anon-supplySet if reachable).
1.5 Chrome contract (added 2026-05-08, updated 2026-05-08 — v103 white)
Added in the v102 cleanup round per
~/.claude/projects/.../memory/feedback_one_card_one_look.md. Updated in v103 — bg flips from.device-card--bg-slateto pure white (no modifier) per user direction (decision Q5 in the v103 plan: cards on#ffffff, no border, no shadow; whitespace separates them on the dashboard). Source of truth forrule:card-bg-consistency+rule:bg-white-cards.
The rule: all 9 heater-card states render the SAME chrome. Only the logical content (badge variant + label, body copy, CTA presence + copy, metric values) differs across states. Chrome means: card wrapper, background, header structure, footer structure, typography roles.
| Chrome element | Locked value | Applies to |
|---|---|---|
| Wrapper | <article class="device-card"> — no --bg-* modifier (default white #ffffff per styles.css .device-card { background: #ffffff }) |
S0, S0u, S1, S2, S3, S4, S6, S7, S8 |
| Width — EVERYWHERE composed (v105+) | width: 100%; max-width: 560px via the default .device-card rule (no modifier; heater-card uses the base class). Applies on /flow/dashboard.html, /flow/dashboard-empty.html, /flow/heater.html, /flow/claim.html, /flow/overview.html, /states.html, /library.html — anywhere the component is composed. Width is a component property, not a wrapper property (per feedback_one_card_one_look.md chrome-consistency rule + the v105 architectural fix). The card brings its own width; wrappers just lay it out. |
every state |
| Position on dashboard (v106+) | RIGHT slot in .dashboard__cards (supply-point-card is the first COMPOSE marker — left, 440 px; heater-card is the second — right, 560 px). v104 had heater-first; v106 reverses to supply-first per reading order where → what. The .dashboard__cards wrapper is a simple flex row (no fixed grid columns) post-v105. |
every state |
| Value-cell wrapping | .device-card__value declarations include white-space: nowrap (v104+) so multi-token values like "47 min 23 s" stay on one line. |
every state with metrics (S6 primarily) |
| Border | NONE — no border: 1px solid … rule on .device-card; cards rely on the .dashboard__cards gap for visual separation |
every state |
| Shadow | NONE — no box-shadow on .device-card |
every state |
| Header | ` |
warning |
| Body | varies — metrics row (S6) / single body paragraph (S0/S0u/S1/S2/S3/S4/S7/S8). The body region is where the state's content lives. | every state |
| Footer | <div class="device-card__footer"> containing (a) primary CTA on the left if the state has one, (b) <a class="device-card__detail-link" href="/flow/heater.html">Detail / Detail</a> on the right |
every state — Detail link is always present; CTA conditional on state |
| Detail link target | /flow/heater.html (renamed from /flow/heater.html in v103; single heater detail page; same target for all 9 states) |
every state |
Forbidden per chrome contract:
- Any
.device-card--bg-*modifier on a heater-card composition. The card has no bg modifier; default white wins. - Border or box-shadow on
.device-card(visual separation comes from.dashboard__cardsgap, not chrome). - Per-state structural divergence (header without name, footer without Detail link, sub-headings, nested cards).
- The device name rendered as
<a>(link). The name is a span; the Detail link in the footer is the canonical detail surface. The name's "click anywhere on the card title" affordance retired in v102. - Inline action buttons in the header (Edit, View, etc.). All actions live in the footer or on the linked detail page.
Enforced by:
rule:card-bg-consistency— re-readstools/generate-states.jsheater-card per-state templates, asserts they all have NO.device-card--bg-*modifier (default white).rule:bg-white-cards(NEW v103) — asserts no.device-card--bg-{slate,cream,sky,mint,gray}modifier appears in any heater-card or supply-point-card per-state template.rule:detail-link-placement— every composed heater-card region has a.device-card__detail-linkinside.device-card__footer.rule:detail-link-target— every.device-card__detail-linkhref targets/flow/<component>.html(here/flow/heater.html).rule:typography-roles— header / metric / period typography matchesspec/typography-roles.md.rule:composition-fidelity— flow-page COMPOSE regions match generator output byte-for-byte; chrome drift in templates surfaces as audit failure across pages.
The §2 state matrix below describes what changes between states — badge, copy, CTA presence + copy. Read it together with this contract.
2. State-combination table (9 rows)
Card heading rule: user-set heater name (likely from WaterHeater.note until a dedicated field exists) || generated default "Water heater" (or "Water heater 2"/"3" for additional heaters in same supply point). Where the heading is the YG code, that's an explicit error state (S0/S0u) where no heater exists to name.
| Tag | Case | Auth | Supply | Runtime | Card heading | Badge | Body line | Primary CTA | Heater-detail page sketch |
|---|---|---|---|---|---|---|---|---|---|
| S0 | (none) | anon | — | unknown code | (the YG code echoed) | not recognized (warn-amber) |
"We don't know this code. Check the label or finish setup in the YG app." | (none — error state) | n/a — code.html error |
| S0u | A | anon | none | Device exists but unpaired | (the YG code echoed) | unpaired (warn-amber) |
"This device isn't on a water heater yet. Contact your installer." | (none — dead end) | Static info card; "what is this?" explainer |
| S1 | B | anon | set | measuring (locked) | water heater name (read-only preview) | set up by installer · claim to access (info-blue) |
"Set a password to take ownership of this device + supply point." | Claim account → /storyboard/claim.html (V17) |
Read-only summary of supply + heater + device, behind a password-set form |
| S2 | C | anon | none | paired | Water heater (default) |
paired (success-green) |
"We're not measuring yet — no supply point. Sign up to track history, or turn on for 3 days." | Turn on for 3 days (LEFT) + body link Sign up / Log in |
Anonymous: no real detail. Card + a "what's next?" explainer. |
| S3 | C | anon | none | boost-on | Water heater (default) |
active · 2d 12h left (success-green) |
"Heater is on until [Sat 17:00]. Sign up before it ends to keep the data." | Sign up (LEFT) + body link Log in |
Live status + recent device pings (client-side cache) + "Sign up to keep this data" |
| S4 | C | loggedIn | none | paired | Water heater (default) |
paired (success-green) |
"We're not measuring yet — no supply point." | + Add supply point (LEFT) → /storyboard/setup.html |
Profile + device summary + "your next step is supply setup" |
| S5 (LOCKED v117 — V19a chosen per Q17) | B/C | loggedIn | set | measuring-fresh (no historical aggregates yet) | water heater name (user-set OR default) | měří se · sbíráme první data / measuring · collecting first data (success-green) |
První údaje za chvíli — historie spotřeby naběhne během prvního týdne. / First readings coming in — consumption history will appear during the first week. |
Turn on for 3 days (LEFT) — same as S6 |
Same shell as S6; metrics block hidden (no .device-card__metrics emitted) until first daily aggregate is available, then card transitions to S6 |
| S6 | B/C | loggedIn | set | measuring | water heater name (user-set OR default) | measuring (success-green) |
Tento měsíc / This month + metrics row |
Turn on for 3 days (LEFT) |
Full detail: hourly chart, monthly history, schedule, settings, "Edit" supply point |
| S7 | C (or B) | loggedIn | set | paused | water heater name | paused (warn-amber) |
"Schedule paused. Last reading 14:22 today." / "Plán pozastaven. Poslední odečet 14:22 dnes." | Resume schedule (LEFT) / Pokračovat v plánu |
Same as S6 + a banner "manually paused since X" + Resume |
| S8 | C (or B) | loggedIn | set | offline | water heater name | offline · last seen 2h ago / offline · naposled před 2 h (danger-red) |
"Heater lost signal. Check power and Wi-Fi." / "Bojler ztratil signál. Zkontrolujte napájení a Wi-Fi." | Troubleshoot (LEFT) / Diagnostika |
Last-known metrics (greyed) + diagnostics checklist + "contact support" |
S5 content (locked v117 — V19a chosen offline; V19b retained in /library.html as archive per the park-as-variant rule):
- Badge (success-green):
měří se · sbíráme první data/measuring · collecting first data - Body:
První údaje za chvíli — historie spotřeby naběhne během prvního týdne./First readings coming in — consumption history will appear during the first week. - Metrics block: hidden (no
.device-card__metricsemitted while no aggregates exist) - CTA:
Zapnout na 3 dny/Turn on for 3 days— same as S6 - Detail link:
/flow/heater.html— same as S6
Why V19a won (recorded for future reference): explicit framing tells the user why there are no metrics yet, instead of letting an ambiguous row of em-dashes do the work. Card naturally shorter than S6, which is acceptable because it disambiguates state — the dashboard layout already handles variable-height cards via flex alignment. V19b's dashed metrics row was technically "same chrome as S6" but semantically conflated "no data yet" with "zero consumption" — a real user could misread it as the heater not measuring.
S7 copy revision (v116): body changed from "Heater is off. Last reading 14:22 today." to "Schedule paused. Last reading 14:22 today." — agrees with CTA verb ("Resume schedule") and is more accurate (YG schedule paused; appliance still draws power on demand). CS: "Topení vypnuto…" → "Plán pozastaven…".
S8 copy revision (v116): body changed from "Device lost signal. Check power and Wi-Fi at the heater." to "Heater lost signal. Check power and Wi-Fi." — drops the technical "Device" in favor of the consumer noun. CS body uses Bojler (the project's CS term for water heater per CLAUDE.md). CTA Troubleshoot / Diagnostika. Badge text condensed.
Badge color rule (decided 2026-04-28, CSS implementation complete 2026-05-16): paused = warn-amber (deliberate user action); offline = danger-red (unintended failure). .badge--danger is registered in spec/components.md:296 and implemented in styles.css:884. No follow-up CSS task remaining.
2.1 — Per-step contract (V2 flow)
V2 flow has 7 steps. Each step has an enforceable contract: the page that renders, the topbar header state expected, the body content, and which (if any) heater-card scenario from Section 2 appears. Storyboard links MUST pass ?step=N so seedForStep(n) (in www/storyboard/v2.js) clears localStorage then seeds deterministically. Direct visits without ?step= are real-user mode.
| Step | Page | Header | Body content | Card scenario | URL |
|---|---|---|---|---|---|
| 1 | code.html |
logged-out | hero code-entry form | (no card — entry) | ?step=1 |
| 2 | overview.html |
logged-out | "we're not measuring yet" + heater card + "Turn on for 3 days" CTA + body links to log in / sign up | S2 | ?step=2 |
| 3 | signup.html |
logged-out | signup form (email + phone + password) | (no card — registration) | ?step=3 |
| 4 | verify.html |
logged-in | verify-code form + email echoed in intro | (no card) | ?step=4 |
| 5 | dashboard.html (empty) |
logged-in | welcome card + paired heater card + "+ Add supply point" CTA | S4 | ?step=5 |
| 6 | setup.html |
logged-in | wizard (supply point → heater → summary) | (no card — wizard) | ?step=6 |
| 7 | dashboard.html (populated) |
logged-in | metrics + measuring heater card + supply-point card | S6 | ?step=7 |
Notes on the table:
- Heater-card scenarios
S2,S4,S6are the rows from Section 2 that the v2 flow actually renders today. The other 6 scenarios (S0, S0u, S1, S3, S7, S8) are design gaps — surfaced by the matrix but with no flow source yet. setup.htmlis the wizard that takes S4 → S6, not a card scenario itself.login.htmlis reached from S2/S4 body links; not part of the V2 flow's 7-step storyboard contract.- Header rule: today,
verify.htmlrenders logged-out (gaprule:topbar-auth#2 inspec/flows.md); the contract says logged-in (email is in store at this point).
Mapping back to existing v2 pages (where to look for the bug source)
overview.htmltoday = S2, currently mis-renders as "measuring" → fix when we render the matrix (Round 5 US-J2). Not in scope for the doc.dashboard.htmlempty branch + paired-device card = S4, today renders the YG code in the heading → should show default "Water heater" per Q4 decision.dashboard.htmlpopulated branch heater card = S6.- No flow page exists for S0, S0u, S1, S3, S7, S8. Those are the design gaps the matrix surfaces. They render as placeholder cards in
/states.html(Round 5 US-J2).
Shelly differences (per-scenario notes)
Shelly devices walk the same shape with these per-row deltas. Shelly is a smart relay (cuts/restores power on a circuit) not a YG meter; affects metric semantics. By the time the user enters a Shelly code on code.html, Shelly-app setup is done. Per-device-type rule (user-confirmed 2026-04-28): no UI difference once setup is done — only setup differs. Same verbs, same badges, same card structure. Internal computation (real wattage vs derived) differs but is invisible to user.
Shelly Case A/B applicability (decided 2026-04-28): skip both. S0u (unpaired Shelly) and S1 (claim from email) don't apply to Shelly — Shelly is always self-installed via Shelly's own app. Shelly's matrix has S2/S3/S4/S6/S7/S8 only.
setup.html wizard — Shelly variant deferred
YG path today: supply → heater → summary (3 steps). Shelly may need an additional step for Shelly-specific config. Step count deferred until Shelly setup.html is being designed; placeholder note: "extra step(s) likely; design when needed".
3. Open questions (status as of 2026-04-28)
Status legend: OPEN = needs decision, ASSUMED = working assumption, please confirm, DEFERRED = will resolve when downstream work starts, RESOLVED = locked.
- Q1 — Case A unpaired-device behavior: RESOLVED (2026-04-28). S0u shows a card-shaped error with
unpairedbadge + "contact installer" body. Detection: Device row exists but noDeviceWaterHeaterwithts_valid_to IS NULL. - Q2 — Badge convention: RESOLVED (2026-04-28). DESCRIPTIVE badges everywhere. ux-design skill principle 9 ("badge the exception") was originally about form fields (mandatory/optional), not status pills — needs scoping fix in the global skill (separate task in TASKS.md).
- Q3 — Boost badge wording during S3: RESOLVED (2026-04-28). Badge =
active · 2d 12h left. (User noted the deeper conversation was about button verb, not the badge — the body line + CTA carry the value-framing.) - Q4 — Default heater name: RESOLVED (2026-04-28).
"Water heater"/"Bojler"for the first/only heater under a supply point; numeric suffix"Water heater N"for the Nth heater (N≥2). Today S4 renders the YG code instead — flagged inconsistency to fix in a future round. - Q5 — Two-account / shared device: ASSUMED RESOLVED, please confirm. Data model permits multi-user via
UserOrganizationM2M pivot under the same customerOrganization. Whether the application enforces single-owner anyway is unknown. If app forbids, this question reopens. ASK API team. - Q6 — Case B claim page (V17): DEFERRED to its own future feature plan. Pattern (data side) is now clear: invite acceptance +
UserOrganization(role=customer)row creation. - Q7 — Paused vs Offline badge color: RESOLVED (2026-04-28). Paused=warn-amber, Offline=danger-red. CSS implementation complete:
.badge--dangeris registered inspec/components.md:296and implemented instyles.css:884(verified 2026-05-16, v116 round). - Q8 — CTA verb consistency across states: DEFERRED. User wants a system-wide copy review session (page name × button × state). TASKS.md follow-up.
- Q9 — Shelly setup wizard step count: DEFERRED until Shelly setup.html is being designed.
- Q10 — Shelly Case A/B applicability: RESOLVED (2026-04-28). Skip both. Shelly matrix is S2/S3/S4/S6/S7/S8 only.
- Q11 — Shelly CTA verb: RESOLVED (2026-04-28). No per-device-type difference. Same verbs everywhere. Setup may differ; running UX is identical.
- Q12 — Pre-website lifecycle error UX: DEFERRED. The badge mechanism extends to pre-web states (e.g.
in setupbadge if API can distinguish). Specific UX resolved when error path is built. - Q13 (NEW) —
manufacturing_statusenum integer values: OPEN. ASK API team. Required to map D0/D1/D2 → real DB values for any error-path implementation that depends on it. - Q14 (NEW) —
WaterHeateruser-set name field: OPEN. ASK API team. Isnote (TextField)repurposed as the user-facing name, or is there a dedicatednamefield elsewhere? If neither, the data model needs a migration to add one. - Q15 (NEW) — Anonymous trial mechanism (S3): OPEN. ASK API team. The schema requires
User+UserOrganizationrows for ownership. How does "Turn on for 3 days" (S3) work for an anonymous user with noUserrow? Options: (a) device-level action keyed byDevice.yg_codeonly, no user data; (b) S3 is actually a stub UI that requires immediate signup; (c) some stateless trial mechanism not visible in the schema. Affects whether S3 is a real scenario or theoretical. - Q16 (NEW) — Multi-heater per supply point: ASSUMED out-of-scope-for-now. Schema permits 1:N (
WaterHeater.supply_pointis FK). Most households are 1:1 in practice. The 9-row matrix assumes 1 heater per household. If multi-heater becomes common, the dashboard's populated branch needs a "multiple heaters" layout — separate plan when needed. Numeric-suffix naming rule already accounts for it. - Q17 (NEW v116, RESOLVED v117) — S5 "measuring, no history yet" state design: RESOLVED 2026-05-17. V19a chosen (explicit
měří se · sbíráme první databadge + first-week body + hidden metrics block). V19b retained in/library.htmlas archive per the park-as-variant rule. S5 row in §2 now LOCKED with V19a content; v117 generator (tools/generate-states.js) emits the S5 template +/states.htmlregenerates with the 9th cell. Production dashboard wiring deferred to a follow-up round that decides the/flow/dashboard.htmlmulti-state rendering pattern (single-card swap via?state=query vs side-by-side gallery vs tabbed) — see TASKS.md.
4. Anchor — data model used by this matrix
Pulled from podklady/db-schema (1).png (Django model diagram, 2026-04-28). Full breakdown lives in ~/.claude/plans/i-want-to-make-federated-star.md "Data model" section + ~/.claude/projects/.../memory/project_data_model.md. Highlights:
- Core chain:
Device ← DeviceWaterHeater ↔ WaterHeater → SupplyPoint → Organization. Time-bounded device-heater link permits swaps. - Multi-tenant:
User ↔ UserOrganization → Organization(withroleenum). Same household → sameOrganization→ multi-user is a data-level capability. - Telemetry:
RawSample,QuantSample,TelemetrySnapshot(the latter hasrelay_outputfor paused/active determination,timestamprecency for offline,rssifor signal quality). - Manufacturing lifecycle:
Device.manufacturing_status(PositiveSmallIntegerField enum) — D0/D1/D2 in this doc map to its values; specific integers TBD.
5. What this matrix proves (and doesn't)
Proves:
- The water heater card has at least 9 distinct meaningful states in the v2 product surface.
- 6 of those states (S0, S0u, S1, S3, S7, S8) currently have NO flow-page representation. That's the design gap.
- The "measuring" badge appearing on overview is a real bug (S2 should be
paired). - Several earlier "open questions" had answers latent in the data model — the schema is more decisive than the UI suggests.
Doesn't prove:
- That every scenario needs its own page in v2 (some might be handled via gating on existing pages).
- The actual visual design — that comes when we render scenarios as state-cards via
tools/generate-states.js. - That the API actually surfaces the data needed to drive each state (Q5/Q13/Q14/Q15 are open).
Next plan (separate)
After this doc lands: a small plan to render each row as a state-card in states.html via the existing generator (tools/generate-states.js + spec/states-catalog.json). The catalog gets 9 new entries; generator runs; states.html shows the matrix visually. From the visual we drive corrections back into v2 flow pages (overview's "measuring" bug, S4's YG-code-vs-default-name, etc.).
A separate plan after that: heater-detail / device-detail pages (the per-card drill-down content sketched in the table's last column).