# Changelog — s2s-client (Azurian Vocero) Tutte le modifiche rilevanti al progetto sono documentate in questo file. Formato basato su [Keep a Changelog](https://keepachangelog.com/it/1.1.0/). --- ## [2026-09-07] ### Aggiunto — Supporto `episode_id` (podcast episode inject) Nuovo parametro query string `?episode_id=` affiancato a `?phoneNumber=` che abilita il caricamento e l'iniezione automatica di un episodio podcast come contesto PRIMARIO nel prompt LLM. **URL di esempio:** ``` /?phoneNumber=%2B447432477433&episode_id=episode-19569f77-a06e-5ce5-a52f-6299dbd67d03 ``` **File modificati:** - **`server.py`** ([server.py](file:///home/duxiter/s2s/s2s-client/server.py)): 1. Aggiunta costante `PODCAST_EPISODE_HINT` ([L426-L429](file:///home/duxiter/s2s/s2s-client/server.py#L426-L429)) per l'hint semantico nel prompt. 2. Aggiunta funzione `_resolve_episode_context(raw_value)` ([L475-L504](file:///home/duxiter/s2s/s2s-client/server.py#L475-L504)): normalizza la risposta API (dict/list/str, eventuale wrapper `{"result": …}`), la serializza in JSON (cap 24.000 chars) e la avvolge con i marker `<<>>`. 3. Aggiunta funzione asincrona `_fetch_podcast_episode(episode_id)` ([L1010-L1078](file:///home/duxiter/s2s/s2s-client/server.py#L1010-L1078)): GET plain verso `https://aquachile.azurianlab.com/api/v1/podcast/episodes/{episode_id}` (timeout 15s, follow redirects, error handling soft — non rompe mai il flusso, restituisce `None` su fail). 4. Estesa `_augment_instructions_with_profile(...)` ([L1137-L1181](file:///home/duxiter/s2s/s2s-client/server.py#L1137-L1181)) con nuovo parametro `episode_context_text`; l'ordine di inject ora è (più specifico → più generico): **episode → profile → reports → summary**. 5. Estesa `_patch_client_session_update_with_profile(...)` ([L1184-L1306](file:///home/duxiter/s2s/s2s-client/server.py#L1184-L1306)) con supporto a `episode_context_text` e logging dedicato `[INJECT-EPISODE]` / `[DEBUG-EPISODE]`. 6. WebSocket connect handler ([L1791-L1959](file:///home/duxiter/s2s/s2s-client/server.py#L1791-L1959)): - legge `episode_id` da query params assieme a `phoneNumber`; - esegue fetch episodio + resolve PRIMA di profile/reports/summary; - salva `injected_episode_context` e `inject_episode_id` nel `session_ctx` condiviso. 7. `_client_to_upstream` ([L2649-L2661](file:///home/duxiter/s2s/s2s-client/server.py#L2649-L2661)): passa `ctx_episode_context` al patch di `session.update`. 8. Endpoint `POST /api/reasoning`: - esteso modello `ReasoningRequest` con campo `episode_id: str | None = None` ([L1309-L1323](file:///home/duxiter/s2s/s2s-client/server.py#L1309-L1323)); - nel handler ([L1411-L1577](file:///home/duxiter/s2s/s2s-client/server.py#L1411-L1577)) fetch episodio indipendente da phone; - `inject_episode_text` viene **prepend** a `user_context_parts` (prima di tutti); - inoltro a NODE_RED / OPENAI con logging esteso (`episodeIdLen`). - **`main.js`** ([main.js](file:///home/duxiter/s2s/s2s-client/main.js)): 1. Lettura da query string: `const voceroEpisodeId = pageParams.get("episode_id")` ([L676-L680](file:///home/duxiter/s2s/s2s-client/main.js#L676-L680)). 2. `fetchOpenAiReasoningAnswer` ([L1337-L1364](file:///home/duxiter/s2s/s2s-client/main.js#L1337-L1364)): campo `episode_id` aggiunto al body POST `/api/reasoning`, logKey e `console.info` estesi. 3. Costruzione URL WebSocket ([L2323-L2342](file:///home/duxiter/s2s/s2s-client/main.js#L2323-L2342)): `episode_id` appeso a query params sia con API `URL.searchParams` sia nel fallback stringa (concat con `&`). --- ### Aggiunto — `S2S_START_SESSION_MODE`: 3 modalità di start sessione Env var letta dal server e propagata al client via `GET /api/config.startSessionMode`. Scopo: governare se e come il microfono si auto-accende e se il modello saluta per primo (flow simile a wake-word). Valori supportati: | Valore | Descrizione | |---|---| | *(vuoto) / default* | Start manuale: solo tap sull'Orb / pulsante Start | | **`TALK`** | Dopo 2s overlay fullscreen "Tocca ovunque per iniziare" (policy safe per Safari/iOS `getUserMedia`). Click/touch/tasto dentro la user gesture → `doStart()`. WS client inietta bootstrap `"hola vocero"` + `requestResponse()`. | | **`TALKFIRST`** | Dopo 2s **tenta `doStart()` DIRETTAMENTE SENZA click**, pre-caricando `pendingWakeWordConfirmationText = "hola vocero"` (stessa variabile della wake-word vera). Se i permessi mic sono già "allow always" funziona senza click. Se fallisce (policy Safari/iOS) → fallback sullo stesso unlocker di TALK con messaggio "permesso microfono". | **File modificati:** - **`server.py`** ([server.py](file:///home/duxiter/s2s/s2s-client/server.py)): 1. Lettura env: `S2S_START_SESSION_MODE = os.environ.get(...)`, `.strip().upper()` ([L150](file:///home/duxiter/s2s/s2s-client/server.py#L150)). 2. Banner `[STARTUP]` esteso con campo `startSessionMode=` ([L196-L212](file:///home/duxiter/s2s/s2s-client/server.py#L196-L212)). 3. Endpoint `GET /api/config` → nuovo campo `startSessionMode` + log `[INTERNAL] /api/config` ([L1383-L1400](file:///home/duxiter/s2s/s2s-client/server.py#L1383-L1400)). - **`index.html`** ([index.html](file:///home/duxiter/s2s/s2s-client/index.html#L32-L39)): - Aggiunto nuovo `#talk-unlocker` overlay fullscreen (z-index 9999), `hidden` di default, con card "Preparando Vocero… / Toca en cualquier lugar para empezar a hablar" + spinner gradient. - **`style.css`** ([style.css](file:///home/duxiter/s2s/s2s-client/style.css#L145-L215)): - Classi `.talk-unlocker`, `.talk-unlocker-hint`, `.talk-unlocker-spinner`, `.talk-unlocker-title`, `.talk-unlocker-sub` + animazioni fade-in e pop-up card. - **`main.js`** ([main.js](file:///home/duxiter/s2s/s2s-client/main.js)): 1. Variabile globale `startSessionMode = ""` + commento ([L628-L632](file:///home/duxiter/s2s/s2s-client/main.js#L628-L632)). 2. Parse dentro `fetchConfig()` → uppercase, log in `reportDebug("B",…)` ([L2309-L2320](file:///home/duxiter/s2s/s2s-client/main.js#L2309-L2320)). 3. Passaggio a WS client → `startWithResponse: startSessionMode === "TALK"` ([L2683-L2692](file:///home/duxiter/s2s/s2s-client/main.js#L2683-L2692)). 4. Blocco scheduling auto-start ([L2976-L3083](file:///home/duxiter/s2s/s2s-client/main.js#L2976-L3083)): - `bootstrapWithAutoStart()`: `await fetchConfig()` → `maybeScheduleAutoStart()`; - `setTimeout(2000 ms)` warmup per finire fetch di episode/profile/reports; - `showTalkUnlocker(fallbackHint)`: gestisce label custom nel caso fallback TALKFIRST; - **`tryTalkFirstDirect()` (TALKFIRST)**: set `pendingWakeWordConfirmationText="hola vocero"` + `doStart()` + try/catch con teardown e fallback unlocker; - listener unlocker click/touchstart/keydown → `startTalkFromGesture()` con `createResumedAudioContext()` dentro la gesture (per iOS silent-free). 5. `onClientStatus()` ([L2878-L2932](file:///home/duxiter/s2s/s2s-client/main.js#L2878-L2932)) aggiunto `hideTalkUnlocker()` in `connecting/queued/your-turn/connected`. - **`ws/s2s-ws-client.js`** ([s2s-ws-client.js](file:///home/duxiter/s2s/s2s-client/ws/s2s-ws-client.js)): 1. Costruttore: nuove proprietà `_startWithResponse`, `_startWithResponseText` (default `"hola vocero"`), `_initialResponseSent` guardia one-shot ([L163-L178](file:///home/duxiter/s2s/s2s-client/ws/s2s-ws-client.js#L163-L178)). 2. Handler `session.updated` ([L688-L713](file:///home/duxiter/s2s/s2s-client/ws/s2s-ws-client.js#L688-L713)): **Solo per `TALK`** (non TALKFIRST, che usa invece `pendingWakeWordConfirmationText`): `this.sendUserText("hola vocero") + this.requestResponse()` — stesso identico code path della wake-word. 3. `requestResponse` / `_createResponseNow` ripristinati puliti (solo payload opzionale `image`; nessun shaped `response.create`). --- ### Modificato — `#reports-sidebar-toggle` visibile solo con `?debug=true` Il bottone "reports" nella topbar (toggle del pannello laterale reports) è ora `data-debug-only="true"` — viene quindi mostrato solo se l'URL contiene `?debug=true`, analogamente al cluster `.topbar-right`. **File modificati:** - **`index.html`** → `#reports-sidebar-toggle` ([index.html](file:///home/duxiter/s2s/s2s-client/index.html#L47-L50)): aggiunto `data-debug-only="true"`. - **`main.js`** → `applyDebugOnlyVisibilityGate()` ([L48-L109](file:///home/duxiter/s2s/s2s-client/main.js#L48-L109)): esteso per iterare *tutti* gli elementi `[data-debug-only="true"]` (non solo `.topbar-right`); riga 1227: `reportsSidebarToggle.hidden = false` gated a `qs.get("debug") === "true"` ([L1227-L1246](file:///home/duxiter/s2s/s2s-client/main.js#L1227-L1246)) + log rosso `🚫 FORZATO HIDDEN` quando manca debug=true. - **`style.css`** → regola di sicurezza già presente: `[data-debug-only]:not([data-debug-enabled]) { display:none !important; visibility:hidden; opacity:0 }` ([L532-L537](file:///home/duxiter/s2s/s2s-client/style.css#L532-L537)). --- ## [2026-09-04] ### Modificato — Chat sempre visibile + integrazione seamless Riorganizzato il layout per mostrare la chat testuale in modo permanente e integrato visivamente con l'Orb nella parte superiore dello stage. - Rimosso pulsante toggle chat nella topbar (`#chat-btn`) e relativo overlay. - Stage con l'Orb nella parte superiore; nuovo `#chat-panel` a seguire (cronologia + form input fisso sopra il footer sticky). - Aggiornata logica in `main.js` e `ui/chat.js` per l'invio testo tramite `client.sendUserText` e auto-scroll senza controlli di visibilità pannello. - Versione banner deploy: **2026-09-03 14:40**. ### Risolto — Stabilità integrazione chat mobile - Errore `Missing element: #chat-btn` risolto con selettori non-strict (`document.querySelector`) per elementi opzionali rimossi. - Refactoring estetico "seamless": chat senza cornici, ombre o sfondi (`background:transparent`, `box-shadow:none`). - **Layout mobile:** - padding incrementato in `#app` (56px) e `.chat-history` (96px) per evitare sovrapposizioni col footer sticky; - margini fissi bolle: `margin-right:22px` (Assistant), `margin-left:6px` (User). - **Auto-scroll streaming:** implementato `_scrollToBottom` con catena di retry multipli (`requestAnimationFrame` + `setTimeout`) per compensare il line-wrap asincrono durante l'arrivo dei token; `overflow-anchor:none` + `scroll-behavior:auto !important`. ### Aggiunto — Avatar utente iniziali + modale profilo - Sostituito testo nome utente nell'header con avatar circolare 40×40px (gradient brand) contenente le iniziali calcolate (es. "AV"). - Nuova `` in `index.html` che si apre al click sull'avatar: mostra email, telefono, ruolo, azienda, con supporto a shape annidate del payload profilo. - Chiusura modale: tasto ESC, click fuori card, pulsante `×` dedicato. --- ## [Note di formato] - **Cap campo prompt episodio:** 24.000 caratteri (transcript lunghi). - **Cap profilo / summary:** 1.600 caratteri ciascuno. - **Cap reports list:** 12.000 caratteri. - **Ordine inject prompt LLM (WS session.update + reasoning):** 1. Episodio podcast corrente (PRIMARIO) 2. Profilo chiamante (identity) 3. Lista reports / script podcast schedulati 4. Summary ultima conversazione - Tutti i blocchi sono marcati con `<<>>` per ispezione log.