Skip to content

Desk Companion Kiosk 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: Build a standalone, glance-first "Desk Companion" Lovelace dashboard (Midnight Glass aesthetic, Layout B) optimized for a kiosked iPad (A3354) on a desk arm.

Architecture: A new YAML-mode Lovelace dashboard (desk_companion.yaml) registered in configuration.yaml, deployed as a config file through the existing CI (ha core restart). One full-screen panel view, root vertical-stack sized to the viewport via card-mod, containing a hero band, a 3-column horizontal-stack, and a house-status strip. Modules are custom:button-card (clock, air ring, energy), custom:mini-graph-card (energy sparkline), custom:mini-media-player (Now Playing), and a chip row.

Tech Stack: Home Assistant YAML Lovelace; HACS cards button-card, mini-graph-card, apexcharts-card (installed) + card-mod, mini-media-player, kiosk-mode (new); native Time & Date integration; existing pytest/yamllint/CI test suite.

Reference spec: docs/superpowers/specs/2026-06-16-desk-companion-kiosk-dashboard-design.md


File Structure

File Responsibility Action
desk_companion.yaml The entire dashboard (one view, all modules) Create
configuration.yaml Register the YAML dashboard under lovelace.dashboards Modify
.github/workflows/ci.yml Add desk_companion.yaml to change-detection + deploy list Modify (lines 56, 113)
tests/conftest.py Scan desk_companion.yaml for entity references Modify (all_referenced_entities, ~line 146)
tests/fixtures/entities.txt Snapshot incl. new sensor.time/sensor.date Regenerate (make snapshot)
docs/desk-kiosk.md + mkdocs.yml Household manual page + kiosk setup Create / Modify

Convention notes: deployable config files live at the repo root (not a subdir). yamllint runs on ./*.yaml, so desk_companion.yaml is linted automatically — keep lines within .yamllint limits (templates use >/block scalars like configuration_tou_addition.yaml). The repo's main dashboard stays storage-mode JSON; this one is intentionally YAML so it's code-built and diffable.


Prerequisites (Louis performs in HA UI — not automatable; verified by later tasks)

Task 0: Install dependencies (manual, one-time)

  • [ ] Step 1: Add the Time & Date integration

In HA: Settings → Devices & Services → + Add Integration → "Time & Date". Enable the Time and Date display options. This creates sensor.time (HH:MM, updates each minute) and sensor.date.

  • [ ] Step 2: Verify the time sensor exists

Run (local shell with HA_URL/HA_TOKEN set):

curl -s -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/states/sensor.time" \
  | python3 -c "import json,sys;print(json.load(sys.stdin)['state'])"
Expected: a time like 14:07 (not an error/null).

  • [ ] Step 3: Install the three HACS frontend cards

In HA: HACS → Frontend → search & install each of: card-mod, mini-media-player, kiosk-mode. HACS auto-registers each as a Lovelace resource. (Reload the browser after.)

  • [ ] Step 4: Verify all three resources are registered

Run:

ssh -o ConnectTimeout=8 root@192.168.4.93 \
  "cat /config/.storage/lovelace_resources" \
  | python3 -c "import json,sys;[print(r['url']) for r in json.load(sys.stdin)['data']['items']]"
Expected: the list now includes card-mod, mini-media-player, and kiosk-mode URLs (alongside mushroom/mini-graph/apexcharts/button-card).


Task 1: Snapshot the new entities into the test fixture

Files: - Modify: tests/fixtures/entities.txt (regenerated)

Build-time resolution: the Time & Date integration was added with the combined time_date display option, so the available entity is sensor.time_date (state like 2026-06-16 14:07, updates each minute) — there is no separate sensor.time/sensor.date. The clock uses sensor.time_date (Task 4). This is fine: it ticks every minute (the re-render trigger we need) and carries both date and time.

  • [ ] Step 1: Regenerate the entity snapshot

With HA_URL/HA_TOKEN exported (so sensor.time_date is captured):

make snapshot

  • [ ] Step 2: Verify the time sensor landed in the fixture

Run:

grep -E '^sensor\.time_date$' tests/fixtures/entities.txt
Expected: sensor.time_date prints.

  • [ ] Step 3: Run the suite (should still pass — nothing references them yet)

Run: make test Expected: all pass.

  • [ ] Step 4: Commit
git add tests/fixtures/entities.txt
git commit -m "auto: snapshot entities — add sensor.time_date for Desk Companion clock"

Task 2: Scaffold the dashboard + wire CI and tests

Goal: a minimal but registered dashboard that deploys and renders, with CI/test plumbing in place — before any styling.

Files: - Create: desk_companion.yaml - Modify: configuration.yaml - Modify: .github/workflows/ci.yml (lines 56, 113) - Modify: tests/conftest.py (all_referenced_entities)

  • [ ] Step 1: Create a minimal desk_companion.yaml
# Desk Companion — kiosk dashboard for the iPad on the desk arm (YAML mode).
# Built in code, not the UI. See docs/superpowers/specs/2026-06-16-desk-companion-kiosk-dashboard-design.md
title: Desk
views:
  - title: Desk
    path: desk
    type: panel
    cards:
      - type: markdown
        content: "Desk Companion  scaffold OK"
  • [ ] Step 2: Register the dashboard in configuration.yaml

Add this top-level block (if a lovelace: key already exists, merge the dashboards: child into it — do not create a second lovelace: key):

lovelace:
  mode: storage          # keep the UI-managed main dashboard working
  dashboards:
    desk-companion:
      mode: yaml
      title: Desk
      icon: mdi:monitor-dashboard
      show_in_sidebar: true
      filename: desk_companion.yaml

  • [ ] Step 3: Add desk_companion.yaml to CI change-detection (line ~56)

In .github/workflows/ci.yml, the changes job's ha_config matcher. Change:

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

  • [ ] Step 4: Add desk_companion.yaml to the deploy file list (line ~113)

Change:

HA_FILES="automations.yaml configuration.yaml configuration_tou_addition.yaml homekit.yaml influxdb_ha.yaml scenes.yaml scripts.yaml"
to:
HA_FILES="automations.yaml configuration.yaml configuration_tou_addition.yaml homekit.yaml influxdb_ha.yaml scenes.yaml scripts.yaml desk_companion.yaml"
(It is not automations.yaml, so any change correctly triggers needs_restart=true → full restart, which a new dashboard registration requires.)

  • [ ] Step 5: Add desk_companion.yaml to the entity-reference scan

In tests/conftest.py, inside all_referenced_entities() (the refs = {...} dict, ~line 146), add this entry after the "lovelace": ... line:

        "desk_companion.yaml": extract_yaml_refs("desk_companion.yaml"),
(Default extraction = entity:/entity_id: keys + states(...)/template-fn refs. No bare_scan — button-card JS templates would create false positives.)

  • [ ] Step 6: Lint + test locally

Run:

yamllint -c .yamllint ./desk_companion.yaml && make test
Expected: yamllint clean; pytest all pass (scaffold references no entities yet).

  • [ ] Step 7: Commit, push, verify deploy

git add desk_companion.yaml configuration.yaml .github/workflows/ci.yml tests/conftest.py
git commit -m "auto: Desk Companion — scaffold YAML dashboard + CI/test wiring (registers /desk-companion)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: test passes, deploy-ha runs (restart). Then open https://<nabu-casa>/desk-companion (or on the iPad) — the "scaffold OK" markdown shows. Stop and confirm it renders before continuing.


Task 3: Midnight Glass base — layout shell + glass style

Goal: the full-screen 3-band grid with frosted-glass placeholder cards, sized to fill the iPad viewport. Replace the markdown scaffold.

Files: - Modify: desk_companion.yaml (replace cards:)

  • [ ] Step 1: Replace the view body with the styled shell

Replace the entire cards: list under the view with:

    cards:
      - type: vertical-stack
        cards:
          # ---- HERO BAND ----
          - type: markdown
            content: " "
            card_mod:
              style: |
                ha-card { {{ glass }} height: 21vh; margin: 0; }
          # ---- THREE COLUMNS ----
          - type: horizontal-stack
            cards:
              - type: markdown
                content: " "
                card_mod: { style: "ha-card { {{ glass }} height: 55vh; }" }
              - type: markdown
                content: " "
                card_mod: { style: "ha-card { {{ glass }} height: 55vh; }" }
              - type: markdown
                content: " "
                card_mod: { style: "ha-card { {{ glass }} height: 55vh; }" }
          # ---- HOUSE STRIP ----
          - type: markdown
            content: " "
            card_mod:
              style: |
                ha-card { {{ glass }} height: 18vh; margin: 0; }
        card_mod:
          style: |
            ha-card { background: none; box-shadow: none; border: none; }
            #root { gap: 12px; }

Note on {{ glass }}: card_mod does not expand Jinja inside style by default the way templates do, and there is no global CSS variable for our glass recipe out of the box. So instead of a {{ glass }} placeholder, inline the recipe in each style block. Use this exact CSS everywhere a glass card is needed (define it once mentally, paste it each time):

background: rgba(255,255,255,0.055);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 14px;
backdrop-filter: blur(9px);
-webkit-backdrop-filter: blur(9px);
color: #e7ecf6;

  • [ ] Step 2: Set the dashboard background + viewport fill (theme-free, via the view)

YAML-mode views can't set a body background directly, so apply it through the root card. Replace the root vertical-stack's card_mod with:

        card_mod:
          style: |
            ha-card {
              background: radial-gradient(130% 120% at 8% -10%, #1b2542 0%, #0a0e1a 58%);
              box-shadow: none; border: none; border-radius: 0;
              min-height: 100vh; padding: 14px;
            }
            #root { gap: 12px; display: flex; flex-direction: column; height: calc(100vh - 28px); }
            #root > hui-horizontal-stack-card { flex: 1 1 auto; }
And in each placeholder's card_mod, drop the fixed height: NNvh (the flex layout now controls it) — keep only the glass recipe from Step 1's note.

  • [ ] Step 3: Lint, commit, push, verify on iPad

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — Midnight Glass shell (hero/3-col/strip, viewport fill)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green. On the iPad: three frosted bands on the navy gradient, filling the screen with no scroll. Confirm the glass effect and full-screen fit before adding modules. If backdrop-filter doesn't render, confirm card-mod is installed (Task 0 Step 4).


Task 4: Time + Weather hero

Files: - Modify: desk_companion.yaml (hero band card)

  • [ ] Step 1: Replace the hero placeholder with a button-card clock + weather

Replace the HERO BAND card with:

          - type: custom:button-card
            entity: sensor.time_date
            show_icon: false
            show_name: false
            custom_fields:
              clock: >
                [[[
                  const td = states["sensor.time_date"].state;          // "YYYY-MM-DD HH:MM"
                  const hhmm = (td.split(" ")[1] || "").slice(0,5);
                  return `<span style="font-size:46px;font-weight:300;letter-spacing:2px;background:linear-gradient(120deg,#fff,#9db4ff);-webkit-background-clip:text;background-clip:text;color:transparent">${hhmm}</span>`;
                ]]]
              date: >
                [[[
                  const td = states["sensor.time_date"].state;
                  const d = new Date(td.replace(" ","T"));
                  return `<span style="font-size:13px;color:#9fb0d0">${
                    d.toLocaleDateString("en-US",{weekday:"long",month:"long",day:"numeric"})}</span>`;
                ]]]
              wx: >
                [[[
                  const o = states["sensor.office_temperature"].state;
                  const out = Math.round(states["weather.forecast_home"].attributes.temperature);
                  const cond = states["weather.forecast_home"].state.replace(/_/g," ");
                  return `<span style="font-size:12px;color:#9fb0d0">Office <b style="color:#e7ecf6;font-size:17px">${Math.round(o)}°</b>
                    &nbsp;&nbsp; Outside <b style="color:#e7ecf6;font-size:17px">${out}°</b> ${cond}</span>`;
                ]]]
            styles:
              card:
                - background: rgba(255,255,255,0.055)
                - border: 1px solid rgba(255,255,255,0.12)
                - border-radius: 14px
                - backdrop-filter: blur(9px)
                - padding: 16px
              grid:
                - grid-template-areas: '"clock wx" "date wx"'
                - grid-template-columns: 1fr auto
                - align-items: center
              custom_fields:
                clock: [justify-self: start]
                date: [justify-self: start, padding-top: 4px]
                wx: [justify-self: end, text-align: right]

  • [ ] Step 2: Lint, commit, push, verify

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — Time + Weather hero (live clock, office/outdoor temp)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green; make test confirms sensor.time, sensor.office_temperature, weather.forecast_home exist. On the iPad: gradient clock ticking each minute, date below, temps right-aligned.


Task 5: Office Air ring (column 1)

Files: - Modify: desk_companion.yaml (column 1)

  • [ ] Step 1: Replace column-1 placeholder with the CO₂ ring + stats button-card
              - type: custom:button-card
                entity: sensor.office_carbon_dioxide
                show_icon: false
                show_name: false
                custom_fields:
                  label: '[[[ return `<span style="font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:#8aa0c8">Office Air</span>` ]]]'
                  ring: |
                    [[[
                      const co2 = Number(states["sensor.office_carbon_dioxide"].state);
                      const pct = Math.min(co2/1600,1)*100;
                      const col = co2 < 800 ? "#22d3ee" : co2 < 1200 ? "#fbbf24" : "#ef4444";
                      return `<div style="width:96px;height:96px;border-radius:50%;
                        background:conic-gradient(${col} 0 ${pct}%,rgba(255,255,255,0.08) ${pct}%);
                        display:flex;align-items:center;justify-content:center">
                        <div style="width:74px;height:74px;border-radius:50%;background:#101830;
                          display:flex;flex-direction:column;align-items:center;justify-content:center">
                          <b style="font-size:20px;color:#e7ecf6">${co2}</b>
                          <s style="font-size:8px;color:#9fb0d0;text-decoration:none">CO₂ ppm</s></div></div>`;
                    ]]]
                  stats: |
                    [[[
                      const voc = Number(states["sensor.office_volatile_organic_compounds"].state);
                      const vcol = voc < 500 ? "#e7ecf6" : voc < 1000 ? "#fbbf24" : "#ef4444";
                      const t = Math.round(Number(states["sensor.office_temperature"].state)*10)/10;
                      const rh = Math.round(Number(states["sensor.office_humidity"].state));
                      const cell = (l,v,c="#e7ecf6")=>`<div style="text-align:center"><div style="font-size:9px;color:#8aa0c8;text-transform:uppercase">${l}</div><b style="font-size:15px;color:${c}">${v}</b></div>`;
                      return `<div style="display:flex;gap:16px;justify-content:center;margin-top:12px">
                        ${cell("VOC",voc,vcol)}${cell("Temp",t+"°")}${cell("RH",rh+"%")}</div>`;
                    ]]]
                styles:
                  card:
                    - background: rgba(255,255,255,0.055)
                    - border: 1px solid rgba(255,255,255,0.12)
                    - border-radius: 14px
                    - backdrop-filter: blur(9px)
                    - padding: 14px
                  grid:
                    - grid-template-areas: '"label" "ring" "stats"'
                    - grid-template-rows: auto 1fr auto
                    - align-items: center
                  custom_fields:
                    label: [justify-self: start]
                    ring: [align-self: center]
  • [ ] Step 2: Lint, commit, push, verify

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — Office Air ring (CO2 thresholds + VOC/temp/RH)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green. On the iPad: a colored CO₂ ring (amber at 908) with VOC/temp/RH below; VOC turns red when > 1000.

Verify before continuing: confirm the office VOC sensor's unit (more-info on sensor.office_volatile_organic_compounds). If it reports ppb not µg/m³, the 500/1000 thresholds may need adjusting — note actual values and tune in Task 9.


Task 6: Energy Now (column 2)

Files: - Modify: desk_companion.yaml (column 2)

  • [ ] Step 1: Replace column-2 placeholder with a vertical-stack: figures + sparkline
              - type: vertical-stack
                cards:
                  - type: custom:button-card
                    entity: sensor.energy_cost_today
                    show_icon: false
                    show_name: false
                    custom_fields:
                      label: '[[[ return `<span style="font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:#8aa0c8">Energy Today</span>` ]]]'
                      cost: '[[[ return `<span style="font-size:34px;font-weight:700;background:linear-gradient(120deg,#34d399,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent">$${Number(states["sensor.energy_cost_today"].state).toFixed(2)}</span>` ]]]'
                      pill: |
                        [[[
                          const peak = states["input_boolean.peak_mode"].state === "on";
                          const rate = Number(states["sensor.tou_rate"].state).toFixed(3);
                          const period = states["sensor.tou_period"].state.replace(/_/g," ");
                          const kw = (Number(states["sensor.whole_home_power"].state)/1000).toFixed(1);
                          const bg = peak ? "rgba(239,68,68,0.16)" : "rgba(52,211,153,0.16)";
                          const bd = peak ? "rgba(239,68,68,0.4)" : "rgba(52,211,153,0.3)";
                          const fg = peak ? "#fca5a5" : "#6ee7b7";
                          const tag = peak ? `PEAK · $${rate}/kWh` : `${period} · $${rate}`;
                          const hint = peak ? `<div style="font-size:10px;color:#fbbf24;margin-top:8px">Hold big appliances ‘til 6pm</div>` : "";
                          return `<div><span style="display:inline-block;font-size:11px;padding:3px 10px;border-radius:20px;background:${bg};border:1px solid ${bd};color:${fg};margin-top:8px">● ${tag}</span>
                            <div style="font-size:10px;color:#9fb0d0;margin-top:8px">Now ${kw} kW · Peak 3–6pm</div>${hint}</div>`;
                        ]]]
                    styles:
                      card:
                        - background: rgba(255,255,255,0.055)
                        - border: 1px solid rgba(255,255,255,0.12)
                        - border-radius: 14px 14px 0 0
                        - backdrop-filter: blur(9px)
                        - padding: 14px 14px 6px
                      grid:
                        - grid-template-areas: '"label" "cost" "pill"'
                        - justify-items: start
                  - type: custom:mini-graph-card
                    entities:
                      - sensor.whole_home_power
                    hours_to_show: 6
                    points_per_hour: 4
                    line_width: 3
                    line_color: '#22d3ee'
                    show:
                      name: false
                      icon: false
                      state: false
                      legend: false
                      fill: fade
                    card_mod:
                      style: |
                        ha-card {
                          background: rgba(255,255,255,0.055);
                          border: 1px solid rgba(255,255,255,0.12);
                          border-top: none;
                          border-radius: 0 0 14px 14px;
                          backdrop-filter: blur(9px);
                          height: 84px; margin: 0; box-shadow: none;
                        }
  • [ ] Step 2: Lint, commit, push, verify

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — Energy Now (cost, TOU pill, live kW, power sparkline)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green (energy_cost_today, tou_period, tou_rate, whole_home_power, input_boolean.peak_mode all in fixture). On the iPad: big green $ figure, an off-peak pill (green) that becomes a red "PEAK" pill 3–6pm weekdays, and a cyan power sparkline beneath.


Task 7: Now Playing (column 3)

Files: - Modify: desk_companion.yaml (column 3)

  • [ ] Step 1: Replace column-3 placeholder with a themed mini-media-player
              - type: custom:mini-media-player
                entity: media_player.office_apple_tv
                artwork: cover
                hide:
                  power: true
                  source: false
                  info: false
                source: icon
                volume_stateless: false
                card_mod:
                  style: |
                    ha-card {
                      background: rgba(255,255,255,0.055);
                      border: 1px solid rgba(255,255,255,0.12);
                      border-radius: 14px;
                      backdrop-filter: blur(9px);
                      color: #e7ecf6; box-shadow: none; height: 100%;
                      --mmp-text-color: #e7ecf6;
                      --mmp-accent-color: #8b5cf6;
                      --mmp-info--media-artist-color: #9fb0d0;
                    }
  • [ ] Step 2: Lint, commit, push, verify on iPad

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — Now Playing (themed mini-media-player, office source)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green. On the iPad: a glassy media card. Play something on the office Apple TV / soundbar and confirm album art + transport + volume respond. If the Apple TV exposes too little control, switch entity: to media_player.office_soundbar (both are in the fixture) and note the choice for the spec.


Task 8: House at a Glance strip

Files: - Modify: desk_companion.yaml (house strip)

  • [ ] Step 1: Replace the house-strip placeholder with a chip-row button-card
          - type: custom:button-card
            show_icon: false
            show_name: false
            triggers_update: all
            custom_fields:
              chips: |
                [[[
                  const chip = (txt,col)=>`<span style="display:inline-flex;align-items:center;gap:6px;font-size:12px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:20px;padding:7px 13px;color:#e7ecf6"><span style="color:${col}">●</span> ${txt}</span>`;
                  // Garage
                  const gOpen = ["cover.right_garage_door_opener_garage_door","cover.left_garage_door_opener_garage_door"]
                    .some(e=>states[e].state==="open");
                  const garage = chip(gOpen?"Garage OPEN":"Garage closed", gOpen?"#ef4444":"#6ee7b7");
                  // Leaks (any wet)
                  const wet = Object.keys(states).filter(e=>e.startsWith("binary_sensor.") &&
                    (e.includes("leak")||e.includes("moisture")) && !e.includes("opening") &&
                    states[e].state==="on");
                  const leaks = chip(wet.length?`Leak: ${wet.length}`:"No leaks", wet.length?"#ef4444":"#6ee7b7");
                  // Presence
                  const home = ["person.louis","person.lindsay"].filter(e=>states[e].state==="home")
                    .map(e=>e.split(".")[1][0].toUpperCase()+e.split(".")[1].slice(1));
                  const who = chip(home.length?home.join(" + ")+" home":"Away", home.length?"#6ee7b7":"#9fb0d0");
                  // Internet
                  const dn = Math.round(Number(states["sensor.speedtest_download"].state));
                  const net = chip(`${dn}↓ Mbps`, dn<150?"#fbbf24":"#6ee7b7");
                  return `<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:center;align-items:center">${garage}${leaks}${who}${net}</div>`;
                ]]]
            styles:
              card:
                - background: rgba(255,255,255,0.055)
                - border: 1px solid rgba(255,255,255,0.12)
                - border-radius: 14px
                - backdrop-filter: blur(9px)
                - padding: 14px
  • [ ] Step 2: Add the chip-referenced entities to the entity scan reliability check

The garage/presence/speedtest entities are referenced as string literals inside a JS template, which extract_yaml_refs will catch via its bare token scan only if enabled — it is not enabled for this file. To keep the test honest, add an explicit comment block of the referenced entities at the top of desk_companion.yaml using entity: keys is overkill; instead verify them manually here:

for e in cover.right_garage_door_opener_garage_door cover.left_garage_door_opener_garage_door \
         person.louis person.lindsay sensor.speedtest_download; do
  grep -qx "$e" tests/fixtures/entities.txt && echo "OK $e" || echo "MISS $e"
done
Expected: all OK. (If any MISS, the entity was renamed — fix the template.)

  • [ ] Step 3: Lint, commit, push, verify

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — House at a Glance strip (garage/leaks/presence/internet)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green. On the iPad: a centered chip row — "Garage closed", "No leaks", "Louis + Lindsay home", "858↓ Mbps" — chips turn red/amber on a real garage-open / leak / slow-link.


Task 9: On-device tuning pass

Files: - Modify: desk_companion.yaml (values only)

  • [ ] Step 1: Tune on the real iPad

With the iPad mounted, adjust values only (no structural change) until it looks right at desk distance: - Band heights (21vh/55vh/18vh ratios, the #root flex) so nothing scrolls and proportions match Layout B. - Font sizes (clock 46px, cost 34px, ring 96px) for legibility from the chair. - Color thresholds: confirm CO₂ (800/1200) and VOC (500/1000, after the unit check in Task 5) match how the room actually reads. - mini-media-player color vars for contrast.

  • [ ] Step 2: Lint, commit, push, verify

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — on-device tuning (sizes, band ratios, thresholds)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green; dashboard looks dialed-in on the iPad.


Task 10: Kiosk chrome-hiding (kiosk-mode)

Files: - Modify: desk_companion.yaml (add kiosk_mode: to the view)

  • [ ] Step 1: Hide HA header + sidebar for this dashboard only

Add to the view (sibling of title/path/type), so the rest of HA keeps its chrome:

  - title: Desk
    path: desk
    type: panel
    kiosk_mode:
      kiosk: true        # hides both header and sidebar
    cards:
      ...

  • [ ] Step 2: Lint, commit, push, verify

yamllint -c .yamllint ./desk_companion.yaml && make test
git add desk_companion.yaml
git commit -m "auto: Desk Companion — kiosk-mode hides HA header/sidebar on /desk-companion"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=ci.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: CI green. On the iPad, /desk-companion shows full-bleed (no HA header/sidebar); other dashboards still have their chrome.


Task 11: Document it (household manual + kiosk setup)

Files: - Create: docs/desk-kiosk.md - Modify: mkdocs.yml (nav entry)

  • [ ] Step 1: Write the manual page

Create docs/desk-kiosk.md:

# Desk Kiosk

A dedicated iPad (on the desk arm) runs the **Desk Companion** dashboard full-screen all day.

## What's on it
- **Clock + weather** — time, date, office & outdoor temp.
- **Office Air** — live CO₂ ring (green/amber/red), plus VOC, temperature, humidity. Amber/red CO₂ = crack a window.
- **Energy Now** — today's electricity cost, the current time-of-use rate, live whole-home power, and a red **PEAK** badge 3–6pm (hold off big appliances).
- **Now Playing** — control office audio; pick another room from the source button.
- **House at a Glance** — garage doors, leak sensors, who's home, internet speed. Anything red needs attention.

## iPad kiosk setup (one-time)
1. Install the **Home Assistant** app; sign in.
2. Open the **Desk** dashboard (sidebar) → `/desk-companion`.
3. **Settings → Accessibility → Guided Access** → On. Open the dashboard, triple-click the side button to lock the iPad to it.
4. **Settings → Display & Brightness → Auto-Lock → Never**; keep it on the charger.
5. Optional: enable **Night Shift** for warmer color in the evening.

To exit kiosk: triple-click, enter the Guided Access passcode.

  • [ ] Step 2: Add it to the nav

In mkdocs.yml, add under the appropriate nav section:

  - Desk Kiosk: desk-kiosk.md

  • [ ] Step 3: Strict docs build, commit, push, verify

# build with the same toolchain CI uses
python3 -m venv /tmp/mkv && /tmp/mkv/bin/pip -q install mkdocs-material mkdocs-minify-plugin
/tmp/mkv/bin/mkdocs build --strict
git add docs/desk-kiosk.md mkdocs.yml
git commit -m "auto: docs — add Desk Kiosk manual page (Desk Companion dashboard + iPad setup)"
git pull --rebase && git push
gh run watch "$(gh run list --workflow=docs.yml --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status --compact
Expected: mkdocs build --strict exit 0; docs workflow deploys.


Task 12: Update the project spec + close the loop

Files: - Modify: docs/superpowers/specs/2026-06-16-desk-companion-kiosk-dashboard-design.md

  • [ ] Step 1: Record build-time resolutions

Append a short "Build Notes" section recording: the VOC unit/threshold decision (Task 5), the chosen media entity (Task 7, Apple TV vs soundbar), and any layout fallback used (stacks vs layout-card). Commit:

git add docs/superpowers/specs/2026-06-16-desk-companion-kiosk-dashboard-design.md
git commit -m "auto: spec — Desk Companion build notes (VOC units, media entity, layout)"
git pull --rebase && git push

  • [ ] Step 2: Final verification checklist (on the iPad)

  • [ ] All five modules render and update live.

  • [ ] Full-screen, no scroll, no HA chrome.
  • [ ] Media transport works.
  • [ ] Peak badge appears 3–6pm; CO₂/VOC/leak/garage colors flip correctly.
  • [ ] make test green; CI green; docs green.

Self-Review (completed by plan author)

Spec coverage: §1 purpose → Tasks 3–10. §2 architecture (YAML mode, CI, tests) → Tasks 2. §3 deps → Task 0. §4 visual system → Task 3 (glass recipe) + per-module styles. §5 layout → Task 3. §6 modules → Tasks 4–8 (one each). §7 kiosk → Tasks 10–11. §8 testing/deploy → every task's lint/make test/CI gate. §9 out-of-scope → respected (no timer/quick-controls/calendar). §10 risks → Task 5 (VOC units), Task 7 (media entity), Task 3 note (layout fallback). No gaps.

Placeholder scan: No TBD/TODO; every step has concrete YAML/commands and expected output. The {{ glass }} shorthand in Task 3 Step 1 is explicitly resolved by the inline-CSS note immediately following.

Type/name consistency: Entity IDs match the spec's Appendix A and the live fixture (sensor.time, sensor.office_carbon_dioxide, sensor.energy_cost_today, input_boolean.peak_mode, media_player.office_apple_tv, cover.*_garage_door_opener_garage_door, person.louis/lindsay, sensor.speedtest_download). CI line numbers (56, 113) and the conftest.py all_referenced_entities target verified against the current files.