Tato stránka zrcadlí spec/device-card-states.md. Sekce 1 = rozhodovací strom (jak kombinace stavú uživatele / zařízení / odběru vede ke scénáři); Sekce 2 = matice 9 scénářů karty bojleru.This page mirrors spec/device-card-states.md. Section 1 = decision tree (how user / device / supply state combinations map to scenarios); Section 2 = state-combination table with 9 scenarios.

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:

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?

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-slate to 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 for rule: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 `
…name…<span class="badge badge--{success
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:

Enforced by:

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):

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:

Mapping back to existing v2 pages (where to look for the bug source)

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.

  1. Q1 — Case A unpaired-device behavior: RESOLVED (2026-04-28). S0u shows a card-shaped error with unpaired badge + "contact installer" body. Detection: Device row exists but no DeviceWaterHeater with ts_valid_to IS NULL.
  2. 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).
  3. 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.)
  4. 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.
  5. Q5 — Two-account / shared device: ASSUMED RESOLVED, please confirm. Data model permits multi-user via UserOrganization M2M pivot under the same customer Organization. Whether the application enforces single-owner anyway is unknown. If app forbids, this question reopens. ASK API team.
  6. 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.
  7. Q7 — Paused vs Offline badge color: RESOLVED (2026-04-28). Paused=warn-amber, Offline=danger-red. CSS implementation complete: .badge--danger is registered in spec/components.md:296 and implemented in styles.css:884 (verified 2026-05-16, v116 round).
  8. Q8 — CTA verb consistency across states: DEFERRED. User wants a system-wide copy review session (page name × button × state). TASKS.md follow-up.
  9. Q9 — Shelly setup wizard step count: DEFERRED until Shelly setup.html is being designed.
  10. Q10 — Shelly Case A/B applicability: RESOLVED (2026-04-28). Skip both. Shelly matrix is S2/S3/S4/S6/S7/S8 only.
  11. Q11 — Shelly CTA verb: RESOLVED (2026-04-28). No per-device-type difference. Same verbs everywhere. Setup may differ; running UX is identical.
  12. Q12 — Pre-website lifecycle error UX: DEFERRED. The badge mechanism extends to pre-web states (e.g. in setup badge if API can distinguish). Specific UX resolved when error path is built.
  13. Q13 (NEW) — manufacturing_status enum integer values: OPEN. ASK API team. Required to map D0/D1/D2 → real DB values for any error-path implementation that depends on it.
  14. Q14 (NEW) — WaterHeater user-set name field: OPEN. ASK API team. Is note (TextField) repurposed as the user-facing name, or is there a dedicated name field elsewhere? If neither, the data model needs a migration to add one.
  15. Q15 (NEW) — Anonymous trial mechanism (S3): OPEN. ASK API team. The schema requires User + UserOrganization rows for ownership. How does "Turn on for 3 days" (S3) work for an anonymous user with no User row? Options: (a) device-level action keyed by Device.yg_code only, 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.
  16. Q16 (NEW) — Multi-heater per supply point: ASSUMED out-of-scope-for-now. Schema permits 1:N (WaterHeater.supply_point is 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.
  17. 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í data badge + first-week body + hidden metrics block). V19b retained in /library.html as 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.html regenerates with the 9th cell. Production dashboard wiring deferred to a follow-up round that decides the /flow/dashboard.html multi-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:


5. What this matrix proves (and doesn't)

Proves:

Doesn't prove:


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).