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>
This commit is contained in:
2026-03-21 19:03:56 +00:00
parent afc182292a
commit c5a8bec2c2

View File

@@ -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 `▶️ <titolo> (<device>)`)
- **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`)