dashboard_apparentati

PRP — Dashboard Appartamento (flat-dashboard)

Versione: 0.15 (Fase 5b: pasti dei giorni successivi sulla TV) Riferimento: docs/PRD.md Data: 2026-09-14, aggiornato 2026-10-13

Questo documento descrive come costruire quanto definito nel PRD: architettura, stack, data model, fasi di lavoro, criteri di accettazione e rischi noti. È pensato per essere dato in pasto a Claude Code come contesto di partenza per l’implementazione, non come specifica immutabile: ogni fase termina con un checkpoint in cui va verificato con l’uso reale prima di passare alla successiva.

1. Architettura generale

[Web app sul telefono, coinquilini]  --scrive (e legge)-->  [Supabase: Postgres]  --legge (polling)-->  [App TV webOS, sola lettura]
        |
   pubblicata su GitHub Pages, aperta da un link condiviso solo in casa

[Attività pianificata, PC fisso di un coinquilino]  --rinnova vicino alla scadenza-->  [Developer Mode sessione TV]
        (ares-cli, rete locale della TV)

[Promemoria di sicurezza, fuori da questo repo]  -->  [Pietro: EXTEND a mano sulla TV]

Punti chiave della scelta architetturale (dettagli e alternative scartate nella conversazione precedente):

2. Stack tecnico

Componente Scelta Note
Datastore condiviso Supabase Free (Postgres + Realtime) Nessun limite di richieste, 500MB DB, realtime via WebSocket incluso. Va “toccato” almeno una volta a settimana o il progetto va in pausa per inattività — con uso quotidiano non è un problema reale.
App per telefono Web app HTML/CSS/JS in mobile-app/, senza framework né build Stesso approccio dell’app TV, ma per browser moderni (Safari su iPhone, Chrome su Android): fetch e async/await. Legge e scrive con le API REST di Supabase; le scritture sono upsert PostgREST (POST con on_conflict e Prefer: resolution=merge-duplicates) su meal_presence, cleaning_shifts e meal_defaults (Fase 3b), le sole tabelle che la RLS apre in scrittura. Ogni upsert manda la riga intera: PostgreSQL controlla NOT NULL e CHECK sulla riga proposta prima di risolvere il conflitto, e absent_has_no_guests_or_time rifiuterebbe un “assente” che si porta dietro orario o ospiti vecchi. Supabase usa PostgREST 14: la documentazione di riferimento è docs.postgrest.org/en/v14, non stable. Gli aggiornamenti degli altri arrivano con polling ogni 10 s mentre la pagina è visibile, più una rilettura subito dopo ogni modifica: niente supabase-js né Realtime, quindi nessuna dipendenza e nessuna migrazione. URL e publishable key arrivano nel frammento del link (#url=…&key=…, che il browser non manda al server) e restano nel localStorage del telefono: non stanno nel repo, e il sito pubblicato senza il link non mostra dati. Le poche funzioni sulle date (lunedì come inizio settimana) sono copiate dall’app TV invece che condivise, per non toccare l’app installata sulla TV. Scartate: Flutter (Android via Obtainium, esclude i 3 iPhone) e app iOS nativa (Apple Developer Program a pagamento).
Distribuzione app GitHub Pages, la stessa pubblicazione dell’app TV Ogni push su main pubblica mobile-app/. mobile-app/make-link.ps1 compone il link dal .env; i coinquilini lo aprono nel browser e lo aggiungono alla schermata Home. Niente APK, keystore né Obtainium. Verificato su un iPhone il 2026-10-06: l’icona aggiunta alla schermata Home si apre come app web e conserva il frammento con la chiave. Per questo la pagina non ha un manifest, il cui start_url sostituirebbe il link. Su iOS 26 l’app web ha una memoria separata da Safari (WebKit), quindi alla prima apertura dall’icona il nome va scelto di nuovo.
App TV webOS homebrew app (HTML/CSS/JS, no framework pesante) Sideload via Developer Mode + ares-cli (pacchetto npm @webos-tools/cli). Legge le API REST di Supabase con polling ogni 20 s, senza supabase-js né Realtime: nessuna dipendenza, nessuna migrazione, e codice con sintassi ES5 + XMLHttpRequest, per non dipendere dalla versione webOS della TV (TV finale: LG OLED48A16LA, webOS 6.5.3-47, motore Chromium 79); usa Promise e flexbox, quindi richiede un motore Chromium (verificato sul simulatore webOS TV 24). URL e publishable key in tv-app/config.js, generato dal .env e git-ignorato. L’app installata è un guscio: index.html, config.js e una copia di riserva di app.js restano sulla TV, mentre all’avvio app.js si carica da GitHub Pages (repo pubblico; il file pubblicato contiene solo codice, nessuna chiave), così gli aggiornamenti non richiedono un nuovo sideload. Screensaver: vedi §5.
Manutenzione Developer Mode Attività pianificata con ares-cli sul PC fisso di un coinquilino, sulla rete della TV La sessione dura circa 1000 ore e scorre anche a TV spenta (999h:55m il 2026-09-16, 838h:02m il 2026-09-23); alla scadenza le app installate in Developer Mode vengono disinstallate e la sessione non si può più prolungare. Il PC di sviluppo non è mai sulla rete della TV, quindi il rinnovo gira sul PC di un coinquilino, acceso quasi sempre o almeno una volta al giorno, con il suo consenso. Meccanismo, solo da fonte community (Dev Manager di webosbrew, issue #256): avviare sulla TV l’app Developer Mode con il parametro di estensione, via luna://com.webos.applicationManager/launch con {"id": "com.palmdts.devmode", "params": {"extend": true}}; verificato a mano sulla TV finale il 2026-09-24 con ares-launch --device tv com.palmdts.devmode --params "{'extend':true}", che manda la stessa chiamata più displayAffinity: 0: Remain Session torna a circa 1000 h. L’app Developer Mode però compare in primo piano e resta aperta: ares-launch --close la rifiuta (chiude solo le app installate in Developer Mode) e getForegroundAppInfo è negato, quindi lo script non può né chiuderla né sapere quale app c’era prima. L’attività (scripts/devmode/) parte all’accesso e ogni ora, ma per rinnovare il meno possibile contatta la TV solo nelle ultime 240 ore della sessione, circa un rinnovo al mese: un file locale tiene l’ora del prossimo controllo, ricalcolata dal residuo letto a ogni rinnovo, e senza quel file (prima esecuzione) rinnova subito. Dopo l’avvio di Developer Mode apre Dashboard casa, che la copre. Se la TV è spenta o irraggiungibile riprova al giro successivo; le 240 ore coprono PC o TV spenti per qualche giorno. Se il rinnovo non sposta il timer riprova dopo 24 ore, per non far comparire Developer Mode a ogni giro; se il residuo non si può leggere, si fida dell’avvio per una sessione intera. Il residuo si legge solo con un metodo community, lo stesso di Dev Manager: token in /var/luna/preferences/devmode_enabled sulla TV, poi developer.lge.com/secure/CheckDevModeSession.dev. Il 2026-09-24 il valore combaciava con quello dell’app Developer Mode prima e dopo il rinnovo, e il token non è cambiato: lo script lo usa per correggere la stima e confermare il rinnovo. Chiave SSH della TV, file di stato e log restano su quel PC, fuori dal repo. L’IP della TV cambia (DHCP): meglio riservarlo sul router (§5). Rete di sicurezza, fuori da questo repo: un promemoria per premere EXTEND prima della scadenza; per spostarsi da solo a ogni rinnovo deve leggere il residuo dallo stesso servizio, con una soglia più bassa di quella dello script. Scartati: script Python con aiowebostv sul PC di sviluppo, che non vede la TV, e la richiesta HTTP a developer.lge.com/secure/ResetDevModeSession.dev, fonte community contestata (risponde “success” ma il timer non si muove).

3. Data model (Supabase / Postgres)

Schema minimo per l’MVP. Nomi tabelle e campi in inglese per coerenza con il resto del codice. La fonte di verità sono i file in supabase/migrations/, da eseguire in ordine; il blocco qui sotto è lo schema che ne risulta.

create table roommates (
  id uuid primary key default gen_random_uuid(),
  name text not null unique,
  created_at timestamptz not null default now(),
  active boolean not null default true
);

create table meal_presence (
  id uuid primary key default gen_random_uuid(),
  roommate_id uuid not null references roommates(id),
  date date not null,
  meal text not null check (meal in ('lunch', 'dinner')),
  is_present boolean not null,
  required_time time,
  guest_names text[] not null default '{}',
  note text,
  updated_at timestamptz not null default now(),
  unique (roommate_id, date, meal),
  constraint absent_has_no_guests_or_time
    check (is_present or (cardinality(guest_names) = 0 and required_time is null))
);

create table cleaning_tasks (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  sort_order int not null default 0,
  active boolean not null default true
);

create table cleaning_shifts (
  id uuid primary key default gen_random_uuid(),
  task_id uuid not null references cleaning_tasks(id),
  roommate_id uuid not null references roommates(id),
  week_start date not null,
  status text not null default 'pending' check (status in ('pending', 'done')),
  updated_at timestamptz not null default now(),
  unique (task_id, week_start)
);

-- Fase 3b
create table meal_defaults (
  roommate_id uuid not null references roommates(id),
  weekday smallint not null check (weekday between 1 and 7),
  meal text not null check (meal in ('lunch', 'dinner')),
  is_present boolean,
  updated_at timestamptz not null default now(),
  primary key (roommate_id, weekday, meal)
);

Note:

4. Fasi di implementazione

Ogni fase ha un checkpoint esplicito: non si passa alla successiva finché quella corrente non è verificata a mano.

Fase 0 — Setup Supabase Creare progetto, schema sopra, RLS di base, popolare roommates e cleaning_tasks a mano. Checkpoint: riuscire a leggere/scrivere righe di test dalla dashboard web di Supabase.

Fase 1 — App TV minima Pagina HTML/JS che legge da Supabase meal_presence (pranzo e cena di oggi, con ospiti e orari richiesti) e cleaning_shifts della settimana corrente (e della successiva il sabato e la domenica se ha già assegnazioni, o col tasto OK), e li mostra aggiornandosi con il polling. Nessuna grafica curata ancora, solo dati veri a schermo. L’app installata è un guscio che carica app.js da GitHub Pages, con la copia nel pacchetto come riserva. Screensaver: nel codice c’è solo il tentativo (1) di §5; il (2) resta un piano. Checkpoint: dati corretti prima in un browser desktop, poi sulla TV dopo il sideload via Developer Mode; una modifica fatta dalla dashboard Supabase compare sulla TV entro 30 s senza toccarla. Osservazione facoltativa, non bloccante: se e dopo quanti minuti di inattività compare lo screensaver con l’app aperta.

Fase 2 — Rinnovo automatico della Developer Mode Comando di rinnovo verificato prima a mano sulla TV, poi script e attività pianificata con ares-cli sul PC del coinquilino (§2). Il promemoria di sicurezza resta fuori da questo repo. Checkpoint: almeno un rinnovo automatico osservato senza intervento manuale (Remain Session torna vicino a 1000h e il log lo registra; il primo giro pianificato rinnova subito, perché il file di stato non c’è ancora) e l’app installata ancora presente.

Fase 3 — App per telefono (web app) MVP Una pagina sola: alla prima apertura si sceglie il proprio nome tra i coinquilini attivi (salvato sul telefono, si può cambiare); presenze ai pasti per oggi e i sei giorni successivi (ci sono / non ci sono, orario richiesto, ospiti come nomi, nota), con la situazione degli altri; turni della settimana corrente e della successiva (assegnazione di ogni area, cambi all’ultimo, segna come fatto). Scrittura diretta su Supabase (§2). Parte in parallelo alla Fase 2, per scelta del 2026-09-30. Scelte del 2026-09-30: ogni coinquilino modifica solo la propria presenza e la salva con un pulsante, compreso ci sono / non ci sono; un campo per ogni ospite; nei turni la scelta del nome e “fatto” si salvano al tocco, chiunque può farlo e “fatto” si annulla; riassegnare un’area la riporta a “da fare”. Senza DELETE una risposta non torna a “non specificato” e un’area assegnata non torna vuota. Checkpoint: node mobile-app/app.test.js passa; da un Android e da un iPhone una modifica compare sulla TV entro 30 s e sull’altro telefono entro 15 s, senza ricaricare.

Fase 3b — Abitudini ai pasti (aggiunta il 2026-10-08) Migrazione meal_defaults (§3): tabella, RLS con lettura, insert e update per anon, grant espliciti (§3, RLS) e ricarica della cache dello schema di PostgREST (notify pgrst, 'reload schema', doc Supabase), eseguita a mano dal SQL Editor come le precedenti. App per telefono: sezione “Abitudini” con, per ogni giorno della settimana, pranzo e cena a scelta tra nessuna, di solito ci sono e di solito non ci sono, salvata con un pulsante in un solo upsert di 14 righe; pasti, conteggi e situazione degli altri usano la risposta del giorno se c’è, altrimenti l’abitudine. App TV: stessa regola per oggi, con le abitudini lette nella stessa richiesta delle presenze; la modifica di tv-app/app.js arriva con un push su main, senza sideload (la TV carica il codice solo all’avvio: dopo il push l’app va chiusa e riaperta), ma la copia di riserva nel pacchetto resta senza abitudini finché non si reinstalla. Checkpoint: node mobile-app/app.test.js e node tv-app/app.test.js passano; con l’abitudine “di solito non ci sono” a pranzo per il giorno di oggi e nessuna risposta, telefono e TV mostrano assente; una risposta “ci sono” per oggi prevale sull’abitudine; togliendo l’abitudine torna “non specificato”.

Fase 4 — Distribuzione Link generato con mobile-app/make-link.ps1 e condiviso solo in casa, con una publishable key solo per i telefoni creata prima di condividerlo (§5). Checkpoint: un coinquilino con Android e uno con iPhone aprono il link su un telefono dove l’app non è mai stata aperta, scelgono il nome, la aggiungono alla schermata Home e la riaprono da lì.

Fase 5 — Rifinitura UI Grafica della dashboard TV sul modello di due bozze di Pietro, una chiara e una scura. I file restano fuori dal repo perché contengono i nomi veri. Le bozze sono un’idea di UI da adattare alla TV; decisioni del 2026-10-13:

Nell’app per telefono cambia solo l’etichetta “Orario richiesto” in “Orario preferito”; il suo stile si decide dopo. Checkpoint: node tv-app/app.test.js e node mobile-app/app.test.js passano; la dashboard si legge dal divano a colpo d’occhio; nel caso peggiore (7 coinquilini con ospiti, orario e nota a pranzo e a cena, più la settimana successiva) tutto entra nello schermo; provata nel browser del PC, nel simulatore webOS TV 24 e sulla TV, in tutti e due i temi; senza lo stile remoto i dati restano leggibili.

Fase 5b — Pasti dei giorni successivi sulla TV (aggiunta il 2026-10-20, su richiesta di un coinquilino) Le schede Pranzo e Cena possono mostrare, oltre a oggi, i 6 giorni successivi, gli stessi dell’app per telefono. Scelte del 2026-10-20:

Fase 6 — Rollout reale Coinvolgere i coinquilini, raccogliere feedback sull’uso reale per una settimana prima di considerarlo “stabile”.

5. Rischi noti e mitigazioni

Rischio Mitigazione
Lo screensaver copre la dashboard Secondo la documentazione ufficiale LG lo screensaver parte sempre, tranne durante un video a schermo intero, e enablePigScreenSaver riguarda solo i video non a schermo intero: da solo non basta. Ordine dei tentativi in Fase 1: (1) chiamata Luna non documentata luna://com.webos.service.tvpower/power/registerScreenSaverRequest con risposta ack: false (solo fonti community); (2) video nero muto in loop a schermo intero dietro la dashboard, solo se lo screensaver dà fastidio nell’uso reale (piano sotto la tabella). "enablePigScreenSaver": false resta in appinfo.json finché il test sulla TV non chiarisce se serve. Da webOS TV 26 le chiamate Luna passano dal controllo requiredACG e la guida ACG non elenca un gruppo per tvpower: su quelle versioni il tentativo (1) può essere rifiutato (Denied method call), e da webOS TV 27 il campo è obbligatorio per tutte le app. La TV finale ha webOS 6.5.3, quindi il controllo ACG non la riguarda: il campo servirebbe solo passando a una TV con webOS 26 o successivo. Verificato sulla TV finale il 2026-09-23: il servizio tvpower esiste e la chiamata non viene rifiutata, ma la registrazione di un’app chiusa resta attiva fino al riavvio della TV e un nuovo tentativo con lo stesso nome fallisce (errorCode -3, “The client is already registered”); per questo il nome del client cambia a ogni avvio. Resta da osservare se la risposta ack: false blocca davvero lo screensaver.
Il caricamento di app.js da GitHub Pages non funziona sulla TV Nessuna documentazione LG dice se un’app locale possa caricare uno script da https: va verificato al primo avvio, e una riga di stato in basso dice quale copia è in uso. Se fallisce, il guscio usa la copia inclusa nel pacchetto e si torna ad aggiornare via sideload. Stessa riserva se la TV è offline o GitHub non risponde.
Sessione Developer Mode scade e l’app sparisce dalla TV Rinnovo automatico dal PC del coinquilino (Fase 2) più promemoria di sicurezza per premere EXTEND a mano; nelle prime settimane controllare Remain Session, senza fidarsi ciecamente dello script al primo giro. Se scade comunque: riattivare la Developer Mode e rifare il sideload (docs/sideload-tv.md).
Il PC del coinquilino o la TV sono spenti quando parte il rinnovo L’attività comincia a tentare 240 ore prima della scadenza e riprova a ogni giro: basta che PC e TV siano accesi insieme una volta in quei 10 giorni. Il promemoria copre i casi peggiori.
L’IP della TV cambia e lo script non la trova Indirizzo riservato alla TV sul router (DHCP reservation), da verificare. Finché manca: se la TV non risponde all’IP registrato, lo script prova la porta 9922 (SSH della Developer Mode) sugli indirizzi della stessa /24 e aggiorna il dispositivo con ares-setup-device --modify; un host sbagliato non passa l’autenticazione SSH con la chiave della TV. ares-setup-device --search non serve: nel sorgente della 3.2.6 è interattivo e si ferma alla prima risposta SSDP che non viene da webOS.
Supabase va in pausa per inattività Con uso quotidiano non dovrebbe accadere. Supabase avvisa per email circa una settimana prima: basta aprire il progetto dalla dashboard (deciso il 2026-09-30, nessun ping automatico). Se va in pausa si riprende dalla dashboard con “Resume project”, con dati e configurazione, entro un anno (doc Supabase).
I coinquilini non usano l’app Rischio reale e non tecnico: basta un link, senza installazioni, ma va spiegato e accompagnato la prima volta.
Il link con la chiave esce di casa Chi lo ha legge i nomi e può scrivere presenze, turni e abitudini, non cancellarli (RLS). Si crea una nuova publishable key in Supabase e si rimanda il link; se è la stessa chiave dell’app TV, anche la TV va reinstallata con un nuovo config.js. Supabase permette più publishable key con nome (pulsante “New publishable key” nel codice di Supabase Studio, non ancora visto nella dashboard): deciso il 2026-10-01 di crearne una solo per i telefoni in Fase 4, così una fuga si risolve cancellandola senza toccare la TV. Dopo un cambio di chiave le icone sulla schermata Home vanno rifatte, perché conservano il link vecchio. Scartati per l’MVP un codice di casa controllato dalla RLS e gli account Supabase Auth (l’SMTP predefinito manda email solo ai membri del team del progetto, 2 all’ora).
Il telefono perde i dati del sito Nome scelto e chiave stanno nel localStorage: se il browser li cancella, basta riaprire il link e riscegliere il nome. In una scheda di Safari iOS li cancella dopo 7 giorni di uso di Safari senza interazione con il sito; l’app web dalla schermata Home ne è esente (WebKit).

Tentativo (2) dello screensaver: piano pronto, non implementato. L’app TV resta un’app normale, aperta quando serve; il video si implementa solo se il tentativo (1) fallisce e lo screensaver dà fastidio nell’uso reale.

  1. Video nero muto, generato una volta (verificato sul PC: H.264 Main, 1920×1080, 25 fps, 10 s, circa 22 KB): ffmpeg -f lavfi -i "color=c=black:s=1920x1080:r=25" -t 10 -c:v libx264 -profile:v main -pix_fmt yuv420p -tune stillimage -an -movflags +faststart tv-app/black-loop.mp4
  2. app.js crea un <video src="black-loop.mp4" autoplay muted loop> a tutto schermo (position: fixed, 100% × 100%, z-index: -1), dietro la dashboard, solo se CONFIG.screensaverVideo è vero.
  3. make-config.ps1 riceve un parametro -Video che scrive screensaverVideo: true in config.js; senza parametro il video resta spento.
  4. Test con la stessa checklist del README (Fase 1, sezione E), annotando anche se la luminosità cala o compaiono controlli di riproduzione. Incognita da documentazione: se il testo sopra il video fa perdere lo stato di “video a schermo intero”.

6. Criteri di accettazione MVP