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'])"
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']]"
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_datedisplay option, so the available entity issensor.time_date(state like2026-06-16 14:07, updates each minute) — there is no separatesensor.time/sensor.date. The clock usessensor.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):
- [ ] Step 2: Verify the time sensor landed in the fixture
Run:
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.yamlto 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$')"
echo "ha_config=$(m '^(automations|configuration|configuration_tou_addition|homekit|influxdb_ha|scenes|scripts|desk_companion)\.yaml$')"
- [ ] Step 4: Add
desk_companion.yamlto 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"
HA_FILES="automations.yaml configuration.yaml configuration_tou_addition.yaml homekit.yaml influxdb_ha.yaml scenes.yaml scripts.yaml desk_companion.yaml"
automations.yaml, so any change correctly triggers needs_restart=true → full restart, which a new dashboard registration requires.) - [ ] Step 5: Add
desk_companion.yamlto the entity-reference scan
In tests/conftest.py, inside all_referenced_entities() (the refs = {...} dict, ~line 146), add this entry after the "lovelace": ... line:
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:
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
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_moddoes not expand Jinja insidestyleby 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 eachstyleblock. Use this exact CSS everywhere a glass card is needed (define it once mentally, paste it each time):
- [ ] 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; }
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
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>
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
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
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
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
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
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
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
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
/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:
- [ ] 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
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 testgreen; 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.