Velký úklid Sheets vrstvy — 28 listů, 6 commitů, jeden den

V pondělí 5. května se ranní rutina vůbec nespustila. Místo toho, abych jen spravil ten konkrétní bug, jsem otevřel celou Google Sheets vrstvu a podíval se, co se v ní vlastně skladuje. Zjištění bylo nepříjemné: polovina listů byla dead code — buď je nikdo neplnil, nebo data lítala někam jinam, než kam měla.

Den skončil 8 commity, Apps Script v18 → v21, opravenými dashboard grafy a 3 listy smazanými ze Sheets. Tady je co všechno se odhalilo a opravilo.

1. Ranní rutina: sleep_window blokoval všechno

Začalo to jednoduchou stížností: "ranní rutina dnes vůbec nezačala". Budík v 3:12, motion sensor v ložnici v 3:16:23, ale ranní brain skript jen zalogoval waiting_2nd_motion a tiše se zasekl.

Root cause: proměnná sh_sleep_window_until_ts byla nastavena na 2026-05-05T06:37:51Z (= 08:37 lokálního času) — 8 hodin od stisku sleep tlačítka pozdě v noci. Ranní open-space router viděl spim=no, sleep_window=yes a všechno blokoval reason= sleep_window_guard. Nikdy se to nesmazalo, když se uživatel probudil — nikdo neměl odpovědnost ten flag vyčistit.

Druhý problém: stejné světla se v noci sama rozsvěcela i přes aktivní sleep_window. Vyšetření: sh_light_router_v1 kontroloval jen sh_spim === 'yes', NE sh_sleep_window_until_ts. Open_space_router měl tu defense už od 3.5., ale light_router ne. Když sh_spim flickeroval na no (kvůli time_sync_guard reset), evening light request prošel a Sektorka1 se rozsvítila ve 22:36.

Fix:

  • sh_wake_confirm_v1 — když multi-zone wake confirm potvrdí probuzení (sh_jsem_vstal=strong_yes), centrálně vyčistit window TS. Single source of truth, ne per-router.
  • sh_light_router_v1 v1.15 — parita s open_space router. Block SLEEP_BLOCKED_REQUESTS i když sleep_window aktivní + strong_yes override.

2. Score producer existoval, ale nikdo ho nečetl

Včera jsem dodal sh_morning_score_v1 — adaptivní engine co každou minutu vyhodnocuje 10 senzorů a vypočítá morning score 0-100 + state machine (night_sleep, pre_wakeup, wakeup_candidate, morning_confirmed, …). Cron 1×/min běžel, vars se aktualizovaly, ale… nikdo z toho nečetl. Brain rozhodoval podle vlastního 180s motion debounce a score ignoroval.

To je proč ranní rutina nezačala. Score by řekl morning_confirmed ve 03:18 (po wake_confirm strong_yes), ale brain nedostal žádný signál pro re-trigger. Single bedroom motion → wait_2nd_motion → 180s window → žádný 2nd motion v ložnici (uživatel šel do koupelny) → debounce nikdy nevypršel.

Fix:

  • sh_morning_brain_v1 — přidán scoreBypass paralelní k existujícímu wakeBypass. Pokud sh_morning_state ∈ {morning_confirmed, wakeup_candidate} a heartbeat čerstvý (<5 min), bypass debounce.
  • sh_morning_score_v1 v1.4 — alarm-aware (probe Homey.alarms.getAlarms(), ±30 min window, +3 score), state transitions logged do EventLogu, dual-log do nového sheetu Morning State Timeline (16 sloupců).

3. Sensor registry — adaptabilita pro budoucí čidla

Druhý nedostatek: senzory v score skriptu byly hardcoded names. Nový sensor v místnosti = edit kódu. PROMPT pro adaptivní rutinu žádal registry pattern.

Implementováno:

  • Nová proměnná sh_wakeup_sensor_registry — JSON s 10 senzory, každý má metadata: device, capability, room, type, morning_weight, night_weight, use_for_wakeup, reliability, rule, is_legacy, note.
  • Populator skript _create_morning_sensor_registry_vars_v1.js — idempotentní create+update.
  • Score iteruje registry, non-legacy entries (is_legacy: false) automaticky přispívají +morning_weight pokud sensor recently active <300s.

Workflow pro nový sensor: edit registru → přidat entry s is_legacy: false, weight, use_for_wakeup: 'yes' → re-run populator. Score automaticky používá od příštího cron ticku. Žádný edit kódu.

4. Dashboard fetchy → 404 (silent)

Audit dashboardů ukázal že 4 endpointy v Apps Script vůbec neexistují, ale dashboard je volá:

?mode=lux_history       → fall-through na default filter
?mode=battery_history    → totéž
?mode=morning_timeline   → totéž
?mode=diagnose           → jen v tooltip (ne fetch)

Apps Script nevrátil 404 — vrátil {rows:[]} (defaultní filter response). Dashboard checkoval data.points nebo data.events (které neexistovaly) a zobrazil "Bez dat (spusť context_full_v1)" hlášku. Vypadalo to jako broken producer, ale ve skutečnosti broken endpoint.

Fix Apps Script v20: 3 nové endpointy:

  • mode=lux_history&hours=24 — Lux Monitor sheet → {points: [{ts, lux_j, lux_k}]}
  • mode=battery_history&days=30 — Battery History → {points: [{ts, device, battery}]}
  • mode=morning_timeline&days=3 — MERGE Morning State Timeline + Rani rutina + TTS Monitor (filtered hour 03-11 Prague) → {events: [...]} sortováno DESC, cap 500.

5. Listy bez producera

Audit Sheets vrstvy: 28 listů celkem, ale realita:

  • 12 aktivních — denní zápis, dashboard čte (EventLog, Zone Activity, TTS Monitor, FlowLog, EnergyLog, …)
  • 4 periodické — depend na události (Daily Report, Gemini Briefing, EnergySummary, Ranni_Rutina_Budik)
  • 6 dead-code routing — data jdou do EventLogu místo dedikovaného listu (System Health, Test Results, Audio Debug, Manual Actions, Logy, Gemini Diagnostics)
  • 5 bez producera vůbec — 0 řádků, ale Apps Script má pro ně handlery (AI Tasks, ExceptionLog, Config, SanityLog, DataQualityReport)
  • 1 čeká — Morning State Timeline (deploy v20)

Z 5 listů bez producera mají 2 diagnostickou hodnotu (ExceptionLog, SanityLog), 3 jsou zbytečné (AI Tasks, Config, DataQualityReport).

ExceptionLog: infrastruktura funguje, jen scripty neselhávají

ExceptionLog list měl 0 řádků, ale 24+ scriptů má logException() v catch blocích. Test: POST přímo přes API → row se zapsala → infrastruktura OK. Empty stav prostě reflektuje produkční stabilitu — scripty neselhávají dost často aby naplnily list. Necháno bez změny, čekající na první real exception.

SanityLog: patch validatoru pro per-item logging

SanityLog měl 0 řádků a 0 producerů. sh_state_validator_v1 už loguje aggregate validator_issues event do EventLogu, ale per-item detail (drift, stuck, conflict) se nikam nepíše.

Patch v1.x: po existujícím EventLog summary log dual-loguje per-item rows do SanityLog (warning_type, severity, zone, details, details_json). Bug E ghost (priority_active=light bez request) bude zachycen při příštím nečistém cycle.

3 listy smazány

Apps Script v21 odstraňuje:

  • AI Tasks — workflow tracker, dashboard čte ale 0 rows nikdy.
  • Config — centralized config nikdy nevyužitý.
  • DataQualityReport — energy data quality denní snapshot, neutilizovaný.

Z Apps Script smazány: SHEETS entries, MAX_ROWS, ensureAllSheets_ headerMap, headers constants, mode=tasks + mode=config endpointy, event_type='ai_task' POST handler. Z Google Sheets manual delete přes pravý klik. Z 5 dashboardů AI Tasks panel display:none + loadAITasks() no-op stub.

6. DeviceInventory: producer nikdy neposílal

DeviceInventory list měl být refreshován každých 6h cron flowem SH – Energy – Device Inventory Cron 6h. Skript sh_device_ inventory_guard_v1 běžel, vars se aktualizovaly, ale list stál 5 dní.

Důvod: skript jen updatuje interní vars (sh_device_measurement_registry_json atd.), ale nikdy POST do Apps Script. Apps Script má handler pro mode=device_inventory POST od v18, ale žádný producer ho nevolal.

Fix v1.1: na konci skriptu přidán fire-and-forget POST registry → Apps Script handleDeviceInventory_ transformuje na batch rows. Po deployi list přijal 82 devices snapshot.

7. Dead-code routing — historický nález

Klíčový architektonický bug: logEvent() lib forsuje sheet: 'EventLog' v payloadu. Apps Script routeEvent_ checkne p.sheet jako PRVNÍ — když je 'EventLog', routes tam a returnuje. event_type routing pro System Health, Manual Actions, Audio Debug, Test Results, Gemini Diagnostics se NIKDY nedosáhne.

Tj. tyhle 6 listů stagnuje 3+ týdny ne proto že producery nefungují, ale proto že data lítá do EventLogu místo. Data nejsou ztracena (jsou v EventLogu), jen v "neoptimálním" listu.

Pragmatické rozhodnutí: nechat jako historickou rezervu. Listy zachované, dashboard je nečte, smazané by byly jen kdyby dělaly nepořádek. Apps Script handlery zachované — pokud bych jednou chtěl fan-out (write to EventLog AND dedicated sheet), je to 3-řádkový patch per sheet.

Final stav

25 listů (z 28 v v20), 3 smazané, 6 commitů, deploy bez incidentu (jeden HomeyScript app crash + recovery — memory bloat ~95 MB / 125 limit, normální v rámci dnešní operational latitude).

Audit: všechny očekávané listy existují, žádný kolaterál. SanityLog + ExceptionLog ready. Morning State Timeline čeká na první state transition. Dashboard panely Lux + Battery + Morning Timeline začaly zobrazovat reálná data po deploy v21.

Lessons

1. Producer-only komponenta je červený alarm. Pokud někdo vyrobí "X produkuje data, Y je zkonzumuje", a Y se nikdy nedoplní — systém má dlouhodobou silent fail tendence. sh_morning_score_v1 běžel měsíc bez konzumenta a nikoho nezajímalo, dokud se ranní rutina nezhroutila.

2. Default values v lib API jsou architektonický bug. sheet: payload.sheet || 'EventLog' vypadalo jako sensible default. Reálně to forsovalo všechny event_type events do jednoho listu a mrtvé routes vznikly přes půl roku bez detekce.

3. Smaž jenom to, co máš jistotu že nikomu nechybí. Z 5 listů bez producera 2 mají diagnostic hodnotu (ExceptionLog, SanityLog). 3 jsou zbytečné. 6 dead-code listů má archivní hodnotu — data nejsou ztracena, jen v jiné lokaci. Smaz konzervativní, audit po deployi povinný.

Mimochodem, pondělní ranní rutina příští týden už proběhne s plnou pipeline: alarm-aware score → state transition log → scoreBypass v brain → registry auto-discovery + Bug E ghost detekce přes SanityLog. Pokud něco selže, EventLog + Morning State Timeline + ExceptionLog dají detailní timeline důvodů.