From c5a8bec2c250e37cc333755f9a7deaec258087e9 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 21 Mar 2026 19:03:56 +0000 Subject: [PATCH] docs: CHANGELOG refactor - Media Agent entry espansa e deduplicata - Entry Blocco A+B riscritta con flusso dettagliato, tabella bug fix, note infrastruttura Radarr/Sonarr/Jellyfin/Ollama - Rimossa sezione duplicata 'Jellyfin Playback Agent Blocco A' (assorbita nell'entry principale) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 145 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f9dcd..71e8515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,52 +4,106 @@ Tutte le modifiche significative al progetto ALPHA_PROJECT sono documentate qui. --- -## [2026-03-21] Media Agent β€” Blocco A+B completo + Calendar Agent fix +## [2026-03-21] Media Agent completo (Blocco A + B1 + B2) + Calendar Agent fix -### Nuovi workflow n8n +### 🎬 Blocco A β€” Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) βœ… -#### 🎬 Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) β€” Blocco A βœ… +Webhook in real-time su PlaybackStart / PlaybackStop da Jellyfin. Workflow attivo e verificato end-to-end con Ghost in the Shell. -Webhook in real-time che intercetta PlaybackStart / PlaybackStop da Jellyfin. +**Flusso:** +``` +Jellyfin Plugin β†’ POST /webhook/jellyfin-playback + β””β†’ πŸ”€ Normalizza Evento (Code β€” JSON.parse body + field mapping) + β””β†’ πŸ”€ Start o Stop? + β”œβ†’ [start] πŸ’Ύ PG - Apri Sessione + πŸ’¬ PG - Msg Start + β””β†’ [stop] πŸ’Ύ PG - Chiudi Sessione + πŸ’¬ PG - Msg Stop +``` -**Bug fix applicati (debugging session):** -1. **Webhook path** β€” `webhookId` deve essere top-level nel nodo JSON (non in `parameters`), altrimenti n8n v2.5.2 in queue mode registra path come `{workflowId}/{nodeName}/{path}` non raggiungibile -2. **SSL Patroni** β€” `NODE_TLS_REJECT_UNAUTHORIZED=0` aggiunto a `n8n-app` e `n8n-app-worker` deployments (Patroni forza `hostssl`) -3. **Postgres queryParams** β€” Postgres node v2 non valuta correttamente `$json.*` in `queryParams` β†’ migrato a SQL inline con `{{ $json.field }}` -4. **Jellyfin body format** β€” il plugin Webhook di Jellyfin invia il body come stringa JSON embedded (`$json.body = '{"ServerId":...}'`) β†’ aggiunto `JSON.parse()` nel Code node -5. **Jellyfin field names** β€” campi reali: `Name` (non `ItemName`), `DeviceName`/`Client`, `UserId` (senza trattini) +**Postgres:** +- `behavioral_context`: INSERT on start (`do_not_disturb=true`, notes: `{item, device, item_type}`), UPDATE on stop (`end_at=now()`, `do_not_disturb=false`) +- `agent_messages`: soggetto `▢️ Ghost in the Shell (Chrome - PC)` / `⏹️ Ghost in the Shell (Chrome - PC)` -**Comportamento verificato:** -- PlaybackStart β†’ INSERT `behavioral_context` (`do_not_disturb=true`) -- PlaybackStop (chiusura player) β†’ UPDATE `end_at`, `do_not_disturb=false` -- `agent_messages` β†’ `▢️ Ghost in the Shell (Chrome - PC)` / `⏹️ …` -- La pausa NON triggerizza PlaybackStop (solo la chiusura del player lo fa) +**Comportamento verificato (test reale):** +- PlaybackStart β†’ INSERT corretto, `do_not_disturb=true` +- PlaybackStop β†’ fired solo alla **chiusura del player** (non alla pausa) β€” comportamento nativo Jellyfin +- Filtro utente: solo `UserId = 42369255a7c64917a28fc26d4c7f8265` (Martin) + +**Bug fix applicati durante il debugging:** + +| # | Problema | Causa | Fix | +|---|---|---|---| +| 1 | Webhook 404 in queue mode | `webhookId` non era top-level nel nodo JSON β†’ n8n genera path dinamico `{workflowId}/{nodeName}/{path}` che il webhook pod non carica | `webhookId` impostato come campo top-level del nodo | +| 2 | SSL Patroni β€” self-signed cert reject | n8n Postgres node usa `rejectUnauthorized=true` di default; Patroni forza `hostssl` | `NODE_TLS_REJECT_UNAUTHORIZED=0` su `n8n-app` e `n8n-app-worker` deployments | +| 3 | `Variable $1 out of range` Postgres | `additionalFields.queryParams` + `$json.*` non funziona in Postgres node v2 di n8n 2.5.2 | Migrato a SQL inline con espressioni `{{ $json.field }}` | +| 4 | Code node filtra tutto β€” `[]` | Jellyfin invia body come **stringa JSON** (`$json.body = '{"ServerId":...}'`), non oggetto | Aggiunto `JSON.parse($json.body)` nel Code node | +| 5 | Campi undefined | Nomi campo reali Jellyfin: `Name`, `DeviceName`/`Client`, `UserId` (senza dashes) | Aggiornati i riferimenti nel Code node | +| 6 | PlaybackStop non arrivava | Il plugin Jellyfin triggerizza Stop solo alla chiusura del player, non alla pausa | Documentato come comportamento atteso | + +**Recupero n8n API token:** il token era corrotto nel contesto LLM (summarizzazione). Recuperato direttamente dal DB n8n: `SELECT "apiKey", label FROM user_api_keys;` sul database `n8n`. --- -#### 🎬 Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) β€” Blocco B1 βœ… +### 🎬 Blocco B1 β€” Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) βœ… -Weekly cron (domenica 03:00). Radarr + Sonarr β†’ GPT-4.1 β†’ Postgres + Qdrant. +Weekly cron (domenica 03:00). Costruisce la **memoria delle preferenze cinematografiche** di Martin. -- Fetch Radarr `/radarr/api/v3/movie` + Sonarr `/sonarr/api/v3/series` -- Merge e normalizzazione: `{type, title, year, genres, status, source, source_id}` -- GPT-4.1 (GitHub Copilot): analisi taste β†’ `{top_genres, preferred_types, library_stats, taste_summary, notable_patterns}` -- Postgres upsert: `memory_facts` (source=`media_library`, source_ref=`media_preferences_summary`, expires +7d) -- Loop per ogni titolo: Ollama `nomic-embed-text` (768-dim) β†’ Qdrant `media_preferences` (Cosine) +**Flusso:** +``` +⏰ Cron domenica 03:00 + β””β†’ 🎬 HTTP Radarr (/radarr/api/v3/movie) ──┐ + β””β†’ πŸ“Ί HTTP Sonarr (/sonarr/api/v3/series) ─── + πŸ”€ Merge Libreria (Code) + β””β†’ πŸ”‘ Token Copilot + β””β†’ πŸ€– GPT-4.1 Analisi + β””β†’ πŸ’Ύ PG Upsert Preferenze + β””β†’ πŸ” Loop Items + β””β†’ πŸ”’ Ollama Embed + β””β†’ πŸ—„οΈ Qdrant Upsert +``` -**Qdrant collection `media_preferences` creata** (PUT `/collections/media_preferences`, vettori 768-dim Cosine). +**Dati estratti dal GPT:** `top_genres`, `preferred_types`, `library_stats`, `taste_summary`, `notable_patterns` + +**Postgres:** `memory_facts` (source=`media_library`, source_ref=`media_preferences_summary`, expires +7d) β€” upsert ON CONFLICT + +**Qdrant `media_preferences`** (collection creata, 768-dim Cosine): +- Embedding: Ollama `nomic-embed-text` su `"{title} {year} {genres} {type}"` +- Payload: `{title, year, type, genres, status, source, source_id, expires_at (+6 mesi)}` +- UtilitΓ  per Pompeo: query semantica tipo *"film sci-fi che piacciono a Martin"* + +**Endpoint interni:** +- Radarr: `http://radarr.media.svc.cluster.local:7878/radarr/api/v3/movie?apikey=922d1405ab1147019d98a2997d941765` (23 film) +- Sonarr: `http://sonarr.media.svc.cluster.local:8989/sonarr/api/v3/series?apikey=22140655993a4ff6bf12314813ec6982` +- Ollama: `http://ollama.ai.svc.cluster.local:11434/api/embeddings` β€” model `nomic-embed-text` (768-dim, multilingual) βœ… operativo +- Qdrant: `http://qdrant.persistence.svc.cluster.local:6333` β€” api-key: sealed secret `qdrant-api-secret` (`__Montecarlo00!`) + +> **Nota infrastruttura**: Radarr e Sonarr girano entrambi nel pod `mediastack` (namespace `media`), ma espongono servizi `ClusterIP` separati. Dall'esterno del cluster le NodePort (30878, 30989) erano irraggiungibili; dall'interno funzionano correttamente. Radarr risponde su `/radarr/` come base path (redirect 307 senza base path). --- -#### 🎞️ Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) β€” Blocco B2 βœ… +### 🎞️ Blocco B2 β€” Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) βœ… -Daily cron (04:00). Jellyfin history β†’ GPT-4.1 β†’ Postgres. +Daily cron (04:00). Costruisce la **memoria della cronologia di visione** di Martin. -- Fetch `/Users/{martin_id}/Items` ultimi 100 played, filtro PlayCount > 0 e last 90 days -- GPT-4.1: `{recent_favorites, preferred_genres, watch_patterns, completion_rate, notes}` -- Postgres upsert: `memory_facts` (source=`jellyfin`, source_ref=`watch_history_summary`, expires +30d) +**Flusso:** +``` +⏰ Cron ogni giorno 04:00 + β””β†’ 🎞️ HTTP Jellyfin (/Users/{id}/Items?Recursive=true&SortBy=DatePlayed&Limit=100) + β””β†’ πŸ”€ Filtra Visti (PlayCount>0, last 90 days) + β””β†’ ❓ Ha Visti? (IF node) + β”œβ†’ [no] β›” Stop + β””β†’ [sΓ¬] πŸ”‘ Token Copilot β†’ πŸ€– GPT-4.1 β†’ πŸ” Parse β†’ πŸ’Ύ PG Upsert +``` -**Jellyfin API token** (`d153606c1ca54574a20d2b40fcf1b02e`) creato via `POST /Auth/Keys?app=Pompeo` con sessione admin (`admin` / `__Montecarlo00!`). +**Dati estratti dal GPT:** `recent_favorites`, `preferred_genres`, `watch_patterns`, `completion_rate`, `notes` + +**Postgres:** `memory_facts` (source=`jellyfin`, source_ref=`watch_history_summary`, expires +30d) + +**Jellyfin API token Pompeo:** +- Creato via `POST /Auth/Keys?app=Pompeo` autenticandosi come `admin` (password `__Montecarlo00!`, auth locale β€” separata dall'Authentik SSO) +- Token: `d153606c1ca54574a20d2b40fcf1b02e` +- Martin UserId: `42369255a7c64917a28fc26d4c7f8265` (da DB SQLite Jellyfin + confermato dai payload webhook) + +> **Nota**: Jellyfin usa Authentik SSO (OIDC) per il login via browser, ma `admin` ha ancora l'auth provider locale attivo. Il token API Γ¨ separato dall'autenticazione SSO e non scade. --- @@ -57,8 +111,17 @@ Daily cron (04:00). Jellyfin history β†’ GPT-4.1 β†’ Postgres. #### πŸ“… Calendar Agent (`4ZIEGck9n4l5qaDt`) β€” 2 bug fix -1. **`πŸ—‘οΈ Cleanup Cancellati` β€” `column "undefined" does not exist`**: l'espressione `.map(i => "'" + i.json.uid.replace(...)+"'")` chiamava `.replace()` su `undefined` quando Parse GPT restituisce `{skip:true}` β†’ l'intera espressione `{{ }}` valutava a `undefined` (non quotato) β†’ PostgreSQL interpretava `undefined` come identificatore. Fix: aggiunto `.filter(i => i.json.uid)` prima del `.map()` + `String()` wrapper. -2. **`πŸ’Ύ Postgres - Salva Evento` β€” `updated_at`**: ON CONFLICT UPDATE includeva `updated_at = NOW()` ma la colonna non esiste in `memory_facts`. Rimosso. +Il workflow falliva ogni 30 minuti con `column "undefined" does not exist`. + +**Bug 1 β€” `πŸ—‘οΈ Cleanup Cancellati`**: quando HA non ha eventi nel range (risposta vuota), Parse GPT restituisce `[{json:{skip:true}}]`. L'espressione nel Cleanup: +```js +.all().map(i => "'" + i.json.uid.replace(/'/g,"''") + "'").join(',') +``` +chiamava `.replace()` su `undefined` (uid non esiste sull'item skip) β†’ l'intera espressione `{{ }}` valutava a `undefined` JavaScript β†’ n8n lo inseriva **senza virgolette** nella SQL β†’ PostgreSQL interpretava `undefined` come nome di colonna. + +Fix: aggiunto `.filter(i => i.json.uid)` prima del `.map()` + `String()` wrapper. + +**Bug 2 β€” `πŸ’Ύ Postgres - Salva Evento`**: ON CONFLICT UPDATE includeva `updated_at = NOW()` ma la colonna `updated_at` non esiste in `memory_facts`. Rimosso dalla clausola DO UPDATE. --- @@ -88,26 +151,6 @@ Ricreate `knowledge` e `episodes` con `size=768` (nomic-embed-text) β€” erano a --- -## [2026-03-21] Jellyfin Playback Agent β€” Blocco A completato - -### Nuovo workflow n8n - -- **`🎬 Pompeo β€” Jellyfin Playback [Webhook]`** (`AyrKWvboPldzZPsM`): riceve webhook da Jellyfin (PlaybackStart / PlaybackStop), filtra per utente `martin` (userId whitelist), e scrive su Postgres: - - **PlaybackStart** β†’ INSERT in `behavioral_context` (`event_type=watching_media`, `do_not_disturb=true`, notes con item/device/session_id) + INSERT in `agent_messages` (soggetto `▢️ ()`) - - **PlaybackStop** β†’ UPDATE su riga aperta piΓΉ recente (`end_at=now()`, `do_not_disturb=false`) + INSERT in `agent_messages` (soggetto `⏹️ ...`) - -### Bug risolti (infrastruttura n8n) - -- **Webhook path n8n v2**: per registrare un webhook con path statico via API, il campo `webhookId` va impostato come attributo top-level del nodo (non dentro `parameters`). Senza di esso n8n genera il path dinamico `{workflowId}/{nodeName}/{path}` che il webhook pod non carica correttamente in queue mode. -- **SSL Postgres / Patroni**: le credential Postgres create via API usavano SSL con `rejectUnauthorized=true` di default, incompatibile con il certificato self-signed di Patroni. Fix: aggiunto `NODE_TLS_REJECT_UNAUTHORIZED=0` ai deployment `n8n-app` e `n8n-app-worker`. -- **queryParams Postgres node**: `additionalFields.queryParams` con espressioni `$json.*` non funziona correttamente in n8n v2.5.2. Fix: valori inline nella SQL via espressioni n8n `{{ $json.field }}`. - -### Configurazione Jellyfin - -- Webhook plugin Jellyfin configurato su `http://n8n-app-webhook.automation.svc.cluster.local/webhook/jellyfin-playback` (POST, eventi: PlaybackStart + PlaybackStop) - ---- - ## [2026-03-21] Daily Digest β€” integrazione memoria Postgres ### Modifiche al workflow `πŸ“¬ Gmail β€” Daily Digest [Schedule]` (`1lIKvVJQIcva30YM`)