Skip to content

Desk Restore + House-Temperatures Dashboard Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Restore the Desk dashboard to its pre-redesign layout and add a new kiosk-capable "Temps" YAML dashboard showing all 8 house temperatures in the established glass + conic-ring design language.

Architecture: Two independent changes. (1) A clean git-level revert of desk_companion.yaml to commit 862acee. (2) A net-new YAML-mode Lovelace dashboard temps_dashboard.yaml — a type: panel view with a header bar, 3 air-quality "hero" ring tiles (Office/Bedroom/Dining) that tap-open native thermostat pop-ups, and a strip of 5 compact temperature ring tiles (Henry/Isaac/Vivian/Annie/Kitchen). Registered in configuration.yaml, the CI deploy file set, and the entity-ref test.

Tech Stack: Home Assistant YAML-mode Lovelace; custom:button-card ([[[ JS ]]] templates for conic rings + threshold colors), custom:bubble-card (hash pop-ups), custom:mini-graph-card, native thermostat card; NemesisRE/kiosk-mode plugin. No build system — the repo test suite (make test) is the gate; deploy is via the ci.yml deploy-ha job on push to main.

Global Constraints

  • Design language (copied verbatim from the 862acee Desk "Office Air" tile): glass card = background: rgba(255,255,255,0.10), border: 1px solid rgba(255,255,255,0.20), border-radius: 14px, backdrop-filter: blur(9px) (+ -webkit- prefix), box-shadow: none, box-sizing: border-box. View background = radial-gradient(130% 120% at 8% -10%, #1b2542 0%, #0a0e1a 58%) fixed.
  • Comfort ring: conic-gradient; fill % = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100; ring color tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444"; inner hole #101830.
  • AQ stat thresholds (heroes only): VOC < 500 #e7ecf6, < 1000 #fbbf24, else #ef4444. CO₂ < 800 #e7ecf6, < 1200 #fbbf24, else #ef4444. RH always #e7ecf6, shown NN%.
  • Occupancy dot: occupied (state === "on") → #34d399 with box-shadow: 0 0 8px #34d399; vacant → rgba(255,255,255,0.14), no glow.
  • Kiosk: every YAML dashboard uses kiosk_mode: { kiosk: '{{ is_state("input_boolean.kiosk_mode", "on") }}' }.
  • No new sensors/automations/HVAC control — display only. Ecobee owns HVAC.
  • All referenced entities already exist in tests/fixtures/entities.txt (verified): the 8 temps, 3 VOC, 3 CO₂, 3 RH, 8 occupancy, climate.office/bedroom/dining_room, sensor.time_date. No entities_allow.txt changes needed.
  • Commit message format: auto: <what changed> [— <why>].
  • Never edit automation_*.yaml fragments. Adding any config file other than automations.yaml triggers a full ha core restart on deploy.

Task 1: Restore the Desk dashboard to commit 862acee

Clean revert — the redesign at 1129eff (2026-06-27) crammed an 8-temperature grid into the Desk; the user wants the original three-column office-ring / energy / now-playing layout back.

Files: - Modify (overwrite): desk_companion.yaml

Interfaces: - Consumes: nothing. - Produces: nothing other tasks depend on (independent change).

  • [ ] Step 1: Confirm the restore target exists and preview it

Run:

git show 862acee:desk_companion.yaml | head -20
Expected: prints the header comment # Desk Companion — kiosk dashboard for the iPad on the desk arm (YAML mode). and title: Desk. If 862acee is not found, run git log --oneline -- desk_companion.yaml and pick the last commit dated 2026-06-16 (the pre-1129eff version).

  • [ ] Step 2: Overwrite the working file with the 862acee version

Run:

git show 862acee:desk_companion.yaml > desk_companion.yaml

  • [ ] Step 3: Verify the diff is the expected revert

Run:

git diff --stat desk_companion.yaml
git diff desk_companion.yaml | head -40
Expected: the file changes back to the three-column layout (look for media_player.office_apple_tv "Now Playing" column and the sensor.office_carbon_dioxide Office Air ring). If git diff shows no changes, the working copy already matched 862acee — skip to Step 6.

  • [ ] Step 4: Lint

Run:

make lint
Expected: yamllint passes (exit 0), no errors on desk_companion.yaml.

  • [ ] Step 5: Entity-ref + structural tests

Run:

make pytest
Expected: PASS. desk_companion.yaml is already registered in tests/conftest.py, and the 862acee version only references entities already in fixtures, so the entity-ref test stays green.

  • [ ] Step 6: Commit
git add desk_companion.yaml
git commit -m "auto: restore Desk dashboard to 862acee — revert 1129eff 8-temp grid back to office-ring / energy / now-playing"

Task 2: Create the new "Temps" dashboard and register it

Build temps_dashboard.yaml (Layout A) and wire it into the config, CI deploy file set, and entity-ref test so it deploys and is gated by the suite.

Files: - Create: temps_dashboard.yaml - Modify: configuration.yaml (add to lovelace.dashboards, after the command-center block at lines 32–37) - Modify: .github/workflows/ci.yml (line 55 change-detect regex; line 135 HA_FILES) - Modify: tests/conftest.py (add to the refs map in all_referenced_entities(), after line 156)

Interfaces: - Consumes: nothing from earlier tasks. - Produces: a type: panel view whose three hero tiles navigate to bubble-card pop-ups keyed #thermo-office, #thermo-bedroom, #thermo-dining. Task 3 adds nothing here; Task 4 documents it.

  • [ ] Step 1: Create temps_dashboard.yaml with exactly this content
# House Temperatures — kiosk dashboard (YAML mode). Built in code, not the UI.
# See docs/superpowers/specs/2026-06-29-desk-temperature-dashboard-design.md
# Layout A: header + 3 air-quality hero ring-tiles (Office/Bedroom/Dining, tap =
# native thermostat pop-up) + a strip of 5 compact ring-tiles (kids + Kitchen).
# Design language carried verbatim from the 862acee Desk "Office Air" tile:
# glass cards, conic comfort ring (green 68-76°, amber 64-80°, red beyond),
# threshold-colored CO₂/VOC/RH cells, green occupancy dot per tile.
title: Temps
kiosk_mode:
  # NemesisRE/kiosk-mode: hide HA header+sidebar only while input_boolean.kiosk_mode
  # is on; the Jinja template is live-reactive so toggling the helper re-shows chrome.
  kiosk: '{{ is_state("input_boolean.kiosk_mode", "on") }}'
views:
  - title: Temps
    path: temps
    type: panel
    background: "radial-gradient(130% 120% at 8% -10%, #1b2542 0%, #0a0e1a 58%) fixed"
    cards:
      - type: vertical-stack
        cards:
          # =====================================================  HEADER
          - type: custom:button-card
            entity: sensor.time_date
            show_icon: false
            show_name: false
            show_state: false
            tap_action:
              action: none
            custom_fields:
              title: '[[[ return `<span style="font-size:24px;font-weight:300;letter-spacing:.5px;background:linear-gradient(120deg,#fff,#9db4ff);-webkit-background-clip:text;background-clip:text;color:transparent">🌡 House Temperatures</span>` ]]]'
              clock: |
                [[[
                  const d = new Date();
                  const h = d.getHours() % 12 || 12;
                  const m = String(d.getMinutes()).padStart(2, "0");
                  const ap = d.getHours() < 12 ? "AM" : "PM";
                  return `<span style="font-size:24px;font-weight:300;color:#e7ecf6">${h}:${m}</span><span style="font-size:13px;color:#9fb0d0;margin-left:6px">${ap}</span>`;
                ]]]
            styles:
              card: &glass
                - background: "rgba(255,255,255,0.10)"
                - border: "1px solid rgba(255,255,255,0.20)"
                - border-radius: 14px
                - backdrop-filter: blur(9px)
                - "-webkit-backdrop-filter": blur(9px)
                - box-shadow: none
                - box-sizing: border-box
                - height: 11vh
                - padding: 0 22px
              grid:
                - grid-template-areas: '"title clock"'
                - grid-template-columns: 1fr auto
                - align-items: center
              custom_fields:
                title:
                  - justify-self: start
                clock:
                  - justify-self: end
          # =====================================================  HERO ROW (3 thermostat zones)
          - type: horizontal-stack
            cards:
              # ---------- OFFICE ----------
              - type: custom:button-card
                entity: sensor.office_carbon_dioxide
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: navigate
                  navigation_path: '#thermo-office'
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.office_occupancy"].state === "on";
                      const dot = `<span style="width:10px;height:10px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:11px;letter-spacing:2px;text-transform:uppercase;color:#8aa0c8">Office</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.office_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      const ring = `<div style="width:194px;height:194px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:154px;height:154px;border-radius:50%;background:#101830;display:flex;flex-direction:column;align-items:center;justify-content:center">
                        <b style="font-size:48px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b><s style="font-size:11px;color:#9fb0d0;text-decoration:none">Office</s></div></div>`;
                      const voc = Number(states["sensor.office_volatile_organic_compounds"].state);
                      const vcol = voc < 500 ? "#e7ecf6" : voc < 1000 ? "#fbbf24" : "#ef4444";
                      const co2 = Number(states["sensor.office_carbon_dioxide"].state);
                      const ccol = co2 < 800 ? "#e7ecf6" : co2 < 1200 ? "#fbbf24" : "#ef4444";
                      const rh = Math.round(Number(states["sensor.office_humidity"].state));
                      const cell = (l, v, c) => `<div style="text-align:center"><div style="font-size:9px;color:#8aa0c8;text-transform:uppercase">${l}</div><b style="font-size:17px;color:${c}">${v}</b></div>`;
                      const stats = `<div style="display:flex;gap:24px;justify-content:center">${cell("VOC",voc,vcol)}${cell("CO₂",co2,ccol)}${cell("RH",rh+"%","#e7ecf6")}</div>`;
                      return `<div style="display:flex;flex-direction:column;align-items:center;gap:24px">${ring}${stats}</div>`;
                    ]]]
                styles:
                  card: &herocard
                    - background: "rgba(255,255,255,0.10)"
                    - border: "1px solid rgba(255,255,255,0.20)"
                    - border-radius: 14px
                    - backdrop-filter: blur(9px)
                    - "-webkit-backdrop-filter": blur(9px)
                    - box-shadow: none
                    - height: 46vh
                    - box-sizing: border-box
                    - padding: 16px
                  grid: &herogrid
                    - grid-template-areas: '"label" "body"'
                    - grid-template-rows: min-content 1fr
                  custom_fields: &herofields
                    label:
                      - justify-self: stretch
                      - align-self: start
                    body:
                      - align-self: center
                      - justify-self: center
              # ---------- BEDROOM ----------
              - type: custom:button-card
                entity: sensor.bedroom_carbon_dioxide
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: navigate
                  navigation_path: '#thermo-bedroom'
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.bedroom_occupancy"].state === "on";
                      const dot = `<span style="width:10px;height:10px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:11px;letter-spacing:2px;text-transform:uppercase;color:#8aa0c8">Bedroom</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.bedroom_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      const ring = `<div style="width:194px;height:194px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:154px;height:154px;border-radius:50%;background:#101830;display:flex;flex-direction:column;align-items:center;justify-content:center">
                        <b style="font-size:48px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b><s style="font-size:11px;color:#9fb0d0;text-decoration:none">Bedroom</s></div></div>`;
                      const voc = Number(states["sensor.bedroom_volatile_organic_compounds"].state);
                      const vcol = voc < 500 ? "#e7ecf6" : voc < 1000 ? "#fbbf24" : "#ef4444";
                      const co2 = Number(states["sensor.bedroom_carbon_dioxide"].state);
                      const ccol = co2 < 800 ? "#e7ecf6" : co2 < 1200 ? "#fbbf24" : "#ef4444";
                      const rh = Math.round(Number(states["sensor.bedroom_humidity"].state));
                      const cell = (l, v, c) => `<div style="text-align:center"><div style="font-size:9px;color:#8aa0c8;text-transform:uppercase">${l}</div><b style="font-size:17px;color:${c}">${v}</b></div>`;
                      const stats = `<div style="display:flex;gap:24px;justify-content:center">${cell("VOC",voc,vcol)}${cell("CO₂",co2,ccol)}${cell("RH",rh+"%","#e7ecf6")}</div>`;
                      return `<div style="display:flex;flex-direction:column;align-items:center;gap:24px">${ring}${stats}</div>`;
                    ]]]
                styles:
                  card: *herocard
                  grid: *herogrid
                  custom_fields: *herofields
              # ---------- DINING ROOM ----------
              - type: custom:button-card
                entity: sensor.dining_room_carbon_dioxide
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: navigate
                  navigation_path: '#thermo-dining'
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.dining_room_occupancy"].state === "on";
                      const dot = `<span style="width:10px;height:10px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:11px;letter-spacing:2px;text-transform:uppercase;color:#8aa0c8">Dining</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.dining_room_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      const ring = `<div style="width:194px;height:194px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:154px;height:154px;border-radius:50%;background:#101830;display:flex;flex-direction:column;align-items:center;justify-content:center">
                        <b style="font-size:48px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b><s style="font-size:11px;color:#9fb0d0;text-decoration:none">Dining</s></div></div>`;
                      const voc = Number(states["sensor.dining_room_volatile_organic_compounds"].state);
                      const vcol = voc < 500 ? "#e7ecf6" : voc < 1000 ? "#fbbf24" : "#ef4444";
                      const co2 = Number(states["sensor.dining_room_carbon_dioxide"].state);
                      const ccol = co2 < 800 ? "#e7ecf6" : co2 < 1200 ? "#fbbf24" : "#ef4444";
                      const rh = Math.round(Number(states["sensor.dining_room_humidity"].state));
                      const cell = (l, v, c) => `<div style="text-align:center"><div style="font-size:9px;color:#8aa0c8;text-transform:uppercase">${l}</div><b style="font-size:17px;color:${c}">${v}</b></div>`;
                      const stats = `<div style="display:flex;gap:24px;justify-content:center">${cell("VOC",voc,vcol)}${cell("CO₂",co2,ccol)}${cell("RH",rh+"%","#e7ecf6")}</div>`;
                      return `<div style="display:flex;flex-direction:column;align-items:center;gap:24px">${ring}${stats}</div>`;
                    ]]]
                styles:
                  card: *herocard
                  grid: *herogrid
                  custom_fields: *herofields
          # =====================================================  KIDS + KITCHEN STRIP (temp-only)
          - type: horizontal-stack
            cards:
              # ---------- HENRY (bedroom_1) ----------
              - type: custom:button-card
                entity: sensor.bedroom_1_temperature
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: more-info
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.bedroom_1_occupancy"].state === "on";
                      const dot = `<span style="width:9px;height:9px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#8aa0c8">Henry</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.bedroom_1_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      return `<div style="width:104px;height:104px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:82px;height:82px;border-radius:50%;background:#101830;display:flex;align-items:center;justify-content:center">
                        <b style="font-size:26px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b></div></div>`;
                    ]]]
                styles:
                  card: &kidcard
                    - background: "rgba(255,255,255,0.10)"
                    - border: "1px solid rgba(255,255,255,0.20)"
                    - border-radius: 14px
                    - backdrop-filter: blur(9px)
                    - "-webkit-backdrop-filter": blur(9px)
                    - box-shadow: none
                    - height: 26vh
                    - box-sizing: border-box
                    - padding: 12px
                  grid: &kidgrid
                    - grid-template-areas: '"label" "body"'
                    - grid-template-rows: min-content 1fr
                  custom_fields: &kidfields
                    label:
                      - justify-self: stretch
                      - align-self: start
                    body:
                      - align-self: center
                      - justify-self: center
              # ---------- ISAAC (bedroom_2) ----------
              - type: custom:button-card
                entity: sensor.bedroom_2_temperature
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: more-info
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.bedroom_2_occupancy"].state === "on";
                      const dot = `<span style="width:9px;height:9px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#8aa0c8">Isaac</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.bedroom_2_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      return `<div style="width:104px;height:104px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:82px;height:82px;border-radius:50%;background:#101830;display:flex;align-items:center;justify-content:center">
                        <b style="font-size:26px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b></div></div>`;
                    ]]]
                styles:
                  card: *kidcard
                  grid: *kidgrid
                  custom_fields: *kidfields
              # ---------- VIVIAN (bedroom_3) ----------
              - type: custom:button-card
                entity: sensor.bedroom_3_temperature
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: more-info
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.bedroom_3_occupancy"].state === "on";
                      const dot = `<span style="width:9px;height:9px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#8aa0c8">Vivian</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.bedroom_3_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      return `<div style="width:104px;height:104px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:82px;height:82px;border-radius:50%;background:#101830;display:flex;align-items:center;justify-content:center">
                        <b style="font-size:26px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b></div></div>`;
                    ]]]
                styles:
                  card: *kidcard
                  grid: *kidgrid
                  custom_fields: *kidfields
              # ---------- ANNIE (bedroom_4) ----------
              - type: custom:button-card
                entity: sensor.bedroom_4_temperature
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: more-info
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.bedroom_4_occupancy"].state === "on";
                      const dot = `<span style="width:9px;height:9px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#8aa0c8">Annie</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.bedroom_4_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      return `<div style="width:104px;height:104px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:82px;height:82px;border-radius:50%;background:#101830;display:flex;align-items:center;justify-content:center">
                        <b style="font-size:26px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b></div></div>`;
                    ]]]
                styles:
                  card: *kidcard
                  grid: *kidgrid
                  custom_fields: *kidfields
              # ---------- KITCHEN (downstairs) ----------
              - type: custom:button-card
                entity: sensor.downstairs_temperature
                show_icon: false
                show_name: false
                triggers_update: all
                tap_action:
                  action: more-info
                custom_fields:
                  label: |
                    [[[
                      const occ = states["binary_sensor.downstairs_occupancy"].state === "on";
                      const dot = `<span style="width:9px;height:9px;border-radius:50%;display:inline-block;background:${occ ? "#34d399" : "rgba(255,255,255,0.14)"};box-shadow:${occ ? "0 0 8px #34d399" : "none"}"></span>`;
                      return `<div style="display:flex;align-items:center;justify-content:space-between;width:100%"><span style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#8aa0c8">Kitchen</span>${dot}</div>`;
                    ]]]
                  body: |
                    [[[
                      const temp = Number(states["sensor.downstairs_temperature"].state);
                      const tpct = Math.max(0, Math.min((temp - 55) / (90 - 55), 1)) * 100;
                      const tcol = (temp >= 68 && temp <= 76) ? "#34d399" : (temp >= 64 && temp <= 80) ? "#fbbf24" : "#ef4444";
                      return `<div style="width:104px;height:104px;border-radius:50%;background:conic-gradient(${tcol} 0 ${tpct}%,rgba(255,255,255,0.08) ${tpct}%);display:flex;align-items:center;justify-content:center">
                        <div style="width:82px;height:82px;border-radius:50%;background:#101830;display:flex;align-items:center;justify-content:center">
                        <b style="font-size:26px;color:#e7ecf6">${Math.round(temp * 10) / 10}°</b></div></div>`;
                    ]]]
                styles:
                  card: *kidcard
                  grid: *kidgrid
                  custom_fields: *kidfields
          # =====================================================  THERMOSTAT POP-UPS
          # Same cards as the Command Overview climate tiles: native thermostat dial
          # + 24h current_temperature trend. Hash-triggered; render hidden until tapped.
          - type: custom:bubble-card
            card_type: pop-up
            hash: '#thermo-office'
            name: Office Thermostat
            icon: mdi:thermometer
            cards:
              - type: thermostat
                entity: climate.office
                features:
                  - type: climate-hvac-modes
                card_mod:
                  style: |
                    ha-card { background: transparent; border: none; box-shadow: none; }
              - type: custom:mini-graph-card
                name: Office temp (24h)
                hours_to_show: 24
                points_per_hour: 2
                entities:
                  - entity: climate.office
                    attribute: current_temperature
          - type: custom:bubble-card
            card_type: pop-up
            hash: '#thermo-bedroom'
            name: Bedroom Thermostat
            icon: mdi:thermometer
            cards:
              - type: thermostat
                entity: climate.bedroom
                features:
                  - type: climate-hvac-modes
                card_mod:
                  style: |
                    ha-card { background: transparent; border: none; box-shadow: none; }
              - type: custom:mini-graph-card
                name: Bedroom temp (24h)
                hours_to_show: 24
                points_per_hour: 2
                entities:
                  - entity: climate.bedroom
                    attribute: current_temperature
          - type: custom:bubble-card
            card_type: pop-up
            hash: '#thermo-dining'
            name: Dining Room Thermostat
            icon: mdi:thermometer
            cards:
              - type: thermostat
                entity: climate.dining_room
                features:
                  - type: climate-hvac-modes
                card_mod:
                  style: |
                    ha-card { background: transparent; border: none; box-shadow: none; }
              - type: custom:mini-graph-card
                name: Dining temp (24h)
                hours_to_show: 24
                points_per_hour: 2
                entities:
                  - entity: climate.dining_room
                    attribute: current_temperature
  • [ ] Step 2: Register the dashboard in configuration.yaml

Add this block to lovelace.dashboards, immediately after the command-center: block (after line 37, before the blank line at 38). Match the existing 4-space/6-space indentation exactly:

    temps:
      mode: yaml
      title: Temps
      icon: mdi:thermometer
      show_in_sidebar: true
      filename: temps_dashboard.yaml
  • [ ] Step 3: Add the file to the CI change-detection regex

In .github/workflows/ci.yml line 55, add temps_dashboard to the alternation (the trailing command_dashboard group). Change:

            echo "ha_config=$(m '^(automations|configuration|configuration_tou_addition|homekit|influxdb_ha|scenes|scripts|desk_companion|plant_dashboard|command_dashboard)\.yaml$')"
to:
            echo "ha_config=$(m '^(automations|configuration|configuration_tou_addition|homekit|influxdb_ha|scenes|scripts|desk_companion|plant_dashboard|command_dashboard|temps_dashboard)\.yaml$')"

  • [ ] Step 4: Add the file to the CI HA_FILES deploy list

In .github/workflows/ci.yml line 135, append temps_dashboard.yaml to the HA_FILES string:

          HA_FILES="automations.yaml configuration.yaml configuration_tou_addition.yaml homekit.yaml influxdb_ha.yaml scenes.yaml scripts.yaml desk_companion.yaml plant_dashboard.yaml command_dashboard.yaml temps_dashboard.yaml"
  • [ ] Step 5: Register the dashboard in the entity-ref test

In tests/conftest.py, inside the refs dict in all_referenced_entities(), add a line after the command_dashboard.yaml entry (line 156). Use the default scan (NOT bare_scan) — like command_dashboard.yaml, this file references entities via entity: and states["..."] bracket syntax, which the default regex catches:

        "command_dashboard.yaml": extract_yaml_refs("command_dashboard.yaml"),
        # default scan: refs are `entity:` + JS states["..."] literals (no string-built ids).
        "temps_dashboard.yaml": extract_yaml_refs("temps_dashboard.yaml"),
  • [ ] Step 6: Lint

Run:

make lint
Expected: yamllint passes on temps_dashboard.yaml and configuration.yaml (exit 0). If yamllint complains about line length, that is acceptable only if the existing dashboards trip the same rule — check .yamllint (the repo disables line-length for these files); if it errors, the rule is active and the offending JS lines must be left as-is only if command_dashboard.yaml already has equally long lines (it does). Resolve any genuine syntax error (e.g. a mis-indented anchor) before proceeding.

  • [ ] Step 7: Entity-ref + structural tests

Run:

make pytest
Expected: PASS. The new temps_dashboard.yaml key is now scanned; every referenced entity (sensor.office_temperature, sensor.office_volatile_organic_compounds, sensor.office_carbon_dioxide, sensor.office_humidity, binary_sensor.office_occupancy, the bedroom/dining equivalents, sensor.bedroom_1..4_temperature, binary_sensor.bedroom_1..4_occupancy, sensor.downstairs_temperature, binary_sensor.downstairs_occupancy, climate.office/bedroom/dining_room, sensor.time_date) is in tests/fixtures/entities.txt, so the entity-ref test stays green. If it fails listing an unknown entity, re-check the exact entity_id spelling against grep <id> tests/fixtures/entities.txt.

  • [ ] Step 8: Container config check (validates the YAML loads in HA)

Run (needs Docker; this is the same gate CI runs before deploy):

make check-config
Expected: check_config passes — confirms HA can parse configuration.yaml with the new temps dashboard pointing at temps_dashboard.yaml. If Docker is unavailable locally, skip and rely on CI's test job, but note the skip.

  • [ ] Step 9: Commit
git add temps_dashboard.yaml configuration.yaml .github/workflows/ci.yml tests/conftest.py
git commit -m "auto: new 'Temps' dashboard — 3 AQ hero rings (tap→thermostat popup) + 5 compact room rings; register + CI + entity-ref test"

Task 3: Document the new dashboard (CLAUDE.md + user manual)

Per the repo rule, a new user-visible dashboard must update CLAUDE.md's dashboard table and the user-facing manual in the same change.

Files: - Modify: CLAUDE.md (the "Dashboards — there are TWO kinds" table + the Pushing Changes to HAOS "Config YAML files" deployable list) - Modify: docs/entities.md (entity reference) and/or docs/climate.md (the temperature/AQ feature page)

Interfaces: - Consumes: the dashboard from Task 2 (url_path: temps, sidebar title "Temps"). - Produces: nothing.

  • [ ] Step 1: Add the "Temps" row to the CLAUDE.md dashboard table

In CLAUDE.md, find the dashboards table (the one with header | Sidebar title | Mode | Repo file | url_path |). Add a row after the Plants row:

| **Temps** | yaml | `temps_dashboard.yaml` | `temps` |
  • [ ] Step 2: Add temps_dashboard.yaml to the deployable-files sentence in CLAUDE.md

In the "Config YAML files — deploy via CI" section, the sentence listing deployable files ends … and the three YAML dashboardscommand_dashboard.yaml,desk_companion.yaml,plant_dashboard.yaml. Update it to read **four** YAML dashboards and append `, `temps_dashboard.yaml. Also update the parenthetical in the "Dashboards — there are TWO kinds" intro that says "and three YAML-mode dashboards" → "and four YAML-mode dashboards", and any "All five dashboards are entity-ref tested" → "All six dashboards…".

  • [ ] Step 3: Add the dashboard to the user manual

In docs/climate.md, add a short subsection (plain language, household audience) describing the new Temps dashboard: a tablet display showing every room's temperature as a colored ring (green = comfortable 68–76°, amber = a bit off, red = extreme), the three main rooms (Office, Bedroom, Dining) also showing air quality (CO₂, VOC, humidity) and opening a full thermostat control when tapped, and a green dot meaning someone's currently in that room. Reference it by its sidebar name "Temps". Keep it to one short admonition/paragraph + a bullet list; do not introduce broken cross-page links (the docs.yml build runs mkdocs build --strict).

  • [ ] Step 4: Build the manual strictly (catches broken links)

Run:

mkdocs build --strict
Expected: builds clean (exit 0). If mkdocs isn't installed locally, skip and rely on the docs.yml CI job, but note the skip. Fix any WARNING/broken-link that --strict turns into an error.

  • [ ] Step 5: Commit
git add CLAUDE.md docs/climate.md
git commit -m "auto: docs — document new Temps dashboard in CLAUDE.md table + climate.md user manual"

Deploy (after all tasks committed)

git push
gh run list --workflow=ci.yml --limit 1   # watch the test→deploy-ha run
- The configuration.yaml change makes deploy-ha do a full ha core restart. - After it goes green, verify on the tablet: the Temps dashboard appears in the sidebar, rings render with correct comfort colors, occupancy dots glow for occupied rooms, and tapping Office/Bedroom/Dining opens the native thermostat pop-up. - The docs/** change triggers the separate docs.yml workflow; verify with gh run list --workflow=docs.yml --limit 1.

Post-deploy visual tuning (note, not a blocker)

vh heights (header 11 / heroes 46 / kids 26) and ring sizes (hero 194/154, kid 104/82) are starting values chosen to fit a typical landscape tablet. If the kids' strip clips or the heroes overflow on the actual desk tablet, adjust the height: values in temps_dashboard.yaml and re-push (a desk_companion.yaml-class change → restart). This is expected dashboard polish, not a correctness issue.


Self-Review

1. Spec coverage: - Restore Desk to 862acee → Task 1. ✅ - New "Temps" dashboard, Layout A (header + 3 heroes + 5 kid tiles) → Task 2, Step 1. ✅ - Design language verbatim (glass, conic ring, thresholds, occupancy dot) → Global Constraints + Task 2 Step 1. ✅ - Hero tiles tap → bubble-card thermostat pop-up (Office/Bedroom/Dining), same cards as Command Overview → Task 2 Step 1 (bubble-cards) + tap_action navigate. ✅ - Kid/Kitchen tiles → more-info → Task 2 Step 1. ✅ - Kids ordered Henry→Isaac→Vivian→Annie, Kitchen last → Task 2 Step 1 (bedroom_1..4 then downstairs). ✅ - Register in configuration.yaml / ci.yml / conftest.py → Task 2 Steps 2–5. ✅ - Kiosk, name Temps, icon mdi:thermometer, url temps → Task 2 Steps 1–2. ✅ - CLAUDE.md table + manual → Task 3. ✅ - Tier-3 entities all present (29 incl. climate) → Global Constraints + Task 2 Step 7. ✅

2. Placeholder scan: No TBD/TODO/"add error handling"/"similar to Task N". The full dashboard YAML is shown verbatim (heroes and kids repeated in full, not referenced). ✅

3. Type consistency: Pop-up hashes match between tile navigation_path and bubble-card hash (#thermo-office/#thermo-bedroom/#thermo-dining). YAML anchors (&herocard/*herocard, &kidcard/*kidcard, etc.) are defined on the first use and referenced consistently. Entity IDs match fixtures exactly. ✅