Registro modifiche

Registro modifiche

Tutte le versioni di Simmetric Chat, dalla più recente alla più datata.

Unreleased

Added

  • Modello di ownership dei widget (2026-10-05 "wown", quick 261005-scn). I widget ora hanno proprietari (il loro creatore) e utenti (grant per-widget): - join widget_users (migrazione additiva) + tetto users.widgetQuotaLimit per proprietario; NULL/fail-closed 0 — le installazioni intoccate vedono zero cambi di comportamento (l'idioma lattice maxSponsoredUsers di Phase-206). - Widget CRUD ri-armato per PROPRIETARI non-admin: POST /api/widgets usa il nuovo permesso widget:manage + il tetto assegnato dall'admin (violazione → 409 { error, quota: "widgets" }); i gate di licenza a livello install (widget_enabled / max_widgets) restano invariati. GET list/detail, PUT settings + binding di workspace raggiungono creator | widget-user | admin (gli id altrui restano 404 non-enumeranti); DELETE resta creator | admin. - Nuova sezione admin nell'area widget (/widgets?tab=quotas, tab Quota): righe per-utente — user | owned | max-widgets — con scritture assign / clear (null), ricerca paginata, audit-log (user.widget_quota.updated). - Nuova tab per-widget "Utenti" (WidgetDetail, in modalità edit): il PROPRIETARIO seleziona gli utenti del widget (PUT replace-set, probe della directory dei grant /widgets/owner-user-search — superficie di selezione identità, niente email); gli utenti con grant ottengono l'arm read-only. - Test: matrice server 30-pin (widgetOwnership.test.ts), armi frontend (WidgetUsersTab/WidgetQuotasTab), percorso e2e (widget-ownership.spec.ts).

Fixed

  • Condividere un workspace imposta il ruolo editor per default, non viewer (2026-10-05 "viewer-default"). Il sintomo riportato — "come utente con grant non posso creare nuove chat né scrivere nelle chat esistenti del workspace condiviso" — è stato ricondotto (via una nuova probe di integrazione a 4 pin, granteeChatWriteAccess.test.ts) al RUOLO del grant, non ai gate: con WORKSPACE_ROLE_ENFORCEMENT=true una riga WorkspaceAccess editor passa GET /chats (200) E POST /chat (200) server-side; una riga VIEWER passa l'arm di lettura ed è valutata 403 sull'arm di scrittura PER DESIGN, e un grantee senza riga ottiene il 404 non-enumerante (SC-4). ENTRAMBE le superfici di grant (il dialog di condivisione del proprietario WorkspaceAccessDialog e il pannello Settings SettingsWorkspaceAccess) pre-selezionavano viewer nei select di ruolo singolo E di massa — un admin che concedeva senza cambiare la tendina produceva grant read-only. Il default dell'UI contraddiceva anche l'intento a livello di schema: il commento di migrazione Phase 189 D-12 fissa il backfill delle righe legacy su editor ("NOT the spec's viewer suggestion"). Entrambe le superfici ora impostano editor come default (singolo + di massa); i pin del cablaggio dei ruoli (un test che sceglie esplicitamente viewer e poi verifica che viewer passi fino in fondo) restano intatti. Nota: gli utenti con grant PRECEDENTI a questa correzione mantengono il ruolo persistito — un ri-grant (upsert del ruolo) ri-valuta.
  • "Riassumi il file allegato" non mente più con la frase preconfezionata di no-risultati (chiusura del gap file-allegato-prima-della-ingest, 2026-10-05). Allegare un file in chat lo carica attraverso la pipeline di ingest ASINCRONA; il messaggio chiede un attimo dopo, il documento ha ancora ZERO chunk, quindi la risoluzione per-route di attachedDocumentId (entrambi gli arm chat in chat.ts) non iniettava NULLA: l'agente ricadeva su rag_search, il documento nuovo non era ancora nel vector store, e la regola 3 di buildSystemPrompt rispondeva con la frase preconfezionata "Non ho trovato informazioni su questo argomento nei documenti del workspace. Verifica che i file pertinenti siano stati caricati e indicizzati." — un'affermazione falsa (il workspace può essere pieno; è solo l'allegato fresco a non essere pronto). Entrambe le sedi di risoluzione ora rilevano l'arm a zero chunk e iniettano un avviso veritiero (buildAttachmentNoticeChunk(new utils/attachmentNotice.ts)): l'arm non-empty-ragContext dell'orchestrator rimuove rag_search, così il modello risponde dall'avviso — "ancora in elaborazione, riprova tra qualche istante" (pending/processing) o "elaborazione fallita/annullata, ricarica" — e l'avviso vieta esplicitamente la frase preconfezionata. Sensibile a stato e progresso (il progresso 0-100 del collector appara solo se > 0); la scansione DLP del rag-context passa sull'avviso senza cambiare. 5 pin unit sull'helper (bracci pending/processing/failed/cancelled + l'istruzione anti-regola-3). Prolungamento frontend — l'ingest waiter del chip di allegato: il chip interroga ogni 1,5s il nuovo endpoint GET /api/documents/:id/attachment-status (tri-check IDOR dei metadati, proiezione { name, status, progress }, senza chunk), mostra spinner + progresso % durante l'ingest, TRATTIENE la barriera di invio (canSend + il percorso Enter via attachmentPending) fino a "completed", e lascia cadere l'allegato con un toast su failed/cancelled (404 → caduta silenziosa; errori di rete transienti continuano il polling — un singhiozzo del server non deve distruggere un ingest sano). La barriera è una cortesia client-side: l'avviso del server resta lo zoccolo veritiero per le schede invecchiate. 4 pin endpoint in documentIdor.test.ts (proiezione access-user + armi D-04 admin); 2 pin ChatInputArea (pending disabilita l'invio + chip di progresso; completed riabilita).
  • Settings → "Accesso ai workspace": le righe di grant mostrano di nuovo il nome dell'utente (Q6-fast settings parity, 2026-10-05). Il dialog di accesso ai workspace aveva corretto questo identico difetto (catena username → id), ma il pannello Settings manteneva la catena PRE-Q6 user ? displayName(user) : userId — e il filo D-15 non invia mai un oggetto user incluso (T-189-11), quindi ogni riga espansa del pannello ricadeva sull'UUID nudo. Il pannello ora rispecchia la catena del dialog (user incluso forma legacy → grant.username → id) e la sua suite fissa la vera forma del filo: il mock statico ha tolto il campo user mai inviato, un nuovo test asserisce che il username viene reso e l'UUID mai (+1 test).
  • Punto d'ingresso del workspace condiviso: le righe della pagina Workspaces ora SI APERTURANO (2026-10-05 "open-row"). Un grantee a livello di workspace (viewer/editor SENZA accesso a progetti) non poteva mai raggiungere il workspace condiviso nell'UI del chat: i selettori della barra laterale sono project-first — la tendina dei progetti usa GET /api/projects (elenca solo i progetti del creatore / ProjectAccess, niente arm di workspace-grant) e la tendina dei workspace è disabled finché un progetto non è selezionato, filtrata per projectId === selectedProjectId — mentre la pagina Workspaces elencava il workspace condiviso (l'OR-filter di GET /api/workspaces include l'arm di grant accessGrants.some(userId)) ma le righe erano solo di gestione, senza modo di entrare. WorkspaceRow ottiene un'azione "Open" (prima nella colonna azioni, resa per OGNI riga elencata — l'appartenenza alla lista implica almeno viewer per l'OR-filter del server; la barriera client è solo UX, le route chat ri-apporta la barriera server-side per richiesta) che monta il workspace via ChatContext (setWorkspaceId(id) + navigate("/") — persiste lastWorkspaceId, così il ripristino al boot rientra nel contesto al reload senza i selettori). Un effetto specchio in App mantiene coerenti le tendine della barra laterale quando il workspace attivo cambia fuori dai selettori — SOLO quando il progetto padre è nella lista progetti dell'utente; la superficie di chat è pilotata dal solo currentWorkspaceId. I selettori della barra laterale ottengono una voce statica "Shared with me" (sentinella __shared__ nella tendina PROJECT, resa solo quando l'insieme di condivisioni non è vuoto): con un progetto selezionato la lista dei workspace resta limitata a quel progetto come prima; scegliendo la sentinella (o con selezione vuota al boot) la tendina dei workspace elenca l'insieme invisibile-per-progetto (tutti i workspace raggiungibili per OR-filter il cui progetto padremanca da GET /api/projects) — la lista di condivisioni del grantee solo-workspace, prima irraggiungibile perché il controllo richiedeva prima un progetto E Radix Select non può svuotare un id progetto concreto una volta scelto (vicolo cieco one-way; la voce nominata è il percorso sempre cliccabile di ritorno). Il placeholder è guidato dal contenuto via l'helper condiviso workspaceSelectPlaceholderKey in sidebar/navModel.tsx (una sentinella vuota obsoleta da una condivisione revocata degrada alla formulazione generica invece di uno scope condiviso abilitato-e-vuoto; workspaceSelectDisabled barriera i mount di AppNavOverlay — l'inline AppSidebarNav si rende sui soli host items). Chiavi i18n workspace.open + sidebar.selectSharedWorkspace + sidebar.sharedProjects aggiunte a tutte le 8 lingue — parità verde. Nuovi pin: le righe viewer mostrano Open e mai delete/edit, un onOpen assente non rende alcun controllo, il pin ride-along di pagina asserisce la scrittura lastWorkspaceId nel localStorage via il vero ChatProvider (WorkspacesPage), l'arm condiviso si rende dagli host items senza progetto e si nasconde quando vuoto (AppSidebarNav), la selezione della sentinella mostra il placeholder condiviso mentre una sentinella obsoleta degrada, e la matrice di gating dell'overlay fissa enabled/disabled per arm + placeholder su desktop e mobile (AppNavOverlay).
  • Il dialog "Accesso" dei workspace (il pulsante in cima alla barra laterale) non straborda più dal proprio bordo (2026-10-05). Ogni blocco full-width sfuggiva oltre il bordo arrotondato del dialog: il DialogContent del ui-kit è grid SENZA grid-template-columns esplicito, quindi i figli sedevano su UNA traccia implicita auto che cresce fino al min-content dell'item più largo — una riga di grant con un nome username UUID ininterrotto di 36 caratteri contava 503–549px contro lo spazio content da 488px (sm:max-w-[520px]), allargando la traccia e STIRANDO ogni item full-width (titolo, riga dei select, checkbox list, <li> di grant) oltre il bordo. Correzione a due strati: la base ui-kit ora fissa una colonna esplicita a minimo-zero (grid-cols-[minmax(0,1fr)] — l'intera famiglia Dialog restringe gli item al box di contenuto limitato invece di spingere fuori la traccia, app-wide) e il sito del dialog aggiunge break-words all titolo interpolato + flex-wrap alle righe grant/bulk e a ogni <li> di grant (i Button nowrap si ripiegano su una loro riga sotto i ~360px di viewport). Verificato con Playwright: zero overflow 380→1280px (i nomi di grant si troncano con ellissi come da progetto, nulla oltrepassa il bordo); il nuovo WorkspaceAccessDialog.test.tsx fissa entrambi gli strati — 174 suite / 1487 test frontend verdi (+1 suite, +4 test).
  • Le righe di grant "Accesso" mostrano di nuovo il NOME dell'utente (Q6 fast follow-up, 2026-10-05). La risposta server D-15 porta {userId, username} ma NON un oggetto user incluso (tolto con T-189-11 per smettere di far trapelare email/ruoli), mentre il dialog rendeva il nome SOLO da quel campo opzionale user morto — ogni riga di utente assegnato ricadeva sull'id UUID. Le righe di grant ora risolvono user incluso (forma legacy) → username → id; WorkspaceAccessGrant ottiene il campo username che il filo invia davvero. Nuovo pin: username reso / l'id mai trapelato / le righe legacy null-username mantengono la catena sull'id (+1 test).
  • Deep link delle Settings restaurato (2026-10-05). Un caricamento completo di /settings (bookmark, voce della barra laterale, voci di navigazione legacy) non apriva più il dialog delle impostazioni dalla revisione R-5 "settings as dialog": al primo commit le query di auth erano ancora in volo (user = null) e l'effetto close-on-logout — dichiarato dopo l'opener nello stesso componente — lo richiudeva nello stesso batch (open-then-cancel), e il pathname post-load era già normalizzato quindi nulla riapriva. La navigazione soft (menu utente) non era coinvolta. L'opener ora parte solo su una sessione LOADED e la normalizzazione preserva la query string, così i deep link ?tab= sopravvivono al mount del dialog; i bookmark ?tab= ora ENTRANO anche nella sezione mirata (l'arm identico a un click su una voce: la pagina di gruppo si apre e la voce marca data-active). e2e/settings-left-menu-nav.spec.ts è di nuovo verde.
  • Anche verificato non-un-bug: POST/DELETE /api/system/settings/branding/icon sono le route del plugin ENTERPRISE (simmetric-enterprise/brandingRoutes, Phase 145, mountProtected + adminLicense("white_label")) — la build community risponde 404 e le UI degradano con il toast documentato di upgrade per design (la grep-guard D-12 whiteLabelExtractionGuard); batteria EE 37 suite / 737 test verdi, guardia community verde.
  • Il pannello Settings non fa più il logout agli utenti non-admin che lo aprono. Il router delle settings era barrato end-to-end con requireAdmin (il permesso admin:settings) mentre il lattice di visibilità per-ruolo (SETTINGS_TAB_PERMISSIONS / RoleSectionVisibility) rendeva comunque sezioni — Profilo, Aspetto, Provider LLM, Avanzate — agli utenti ordinari. Aprire il pannello (o una sezione) faceva fetch alla shell di SettingsPage su GET /api/system/settings, ricevendo 403, e la pagina confondeva 403 con 401: mostrava "Session expired. Please log in again.", cancellava il token e redirigeva a /login 3 secondi dopo — distruggendo una sessione interamente valida per un semplice gap di permessi. Correzione in due parti (2026-10-04 "settings-panel logout"): - Server: GET /api/system/settings ora è permission-aware. Gli amministratori (portatori di admin:settings) mantengono l'intero payload console byte-identico al prima; ogni altro utente autenticato riceve solo il sottoinsieme NON_ADMIN_SETTINGS_READ_KEYS (condiviso) — le chiavi non-sensibili consumate dalle superfici raggiungibili dai non-admin (BRANDING_* per la tab Aspetto / l'header app, ALLOW_NON_ADMIN_UPLOAD per la pagina uploads) — ogni voce readOnly: true (regola anti-mismatch: non rendere mai un form che il server batterebbe con 403). I segreti (SMTP_PASS, LLM_API_KEY, JWT_SECRET, DATABASE_URL, VECTOR_DB_API_KEY, ...) non possono mai raggiungere una risposta non-admin. La vista org-scoped (?organizationId=) e tutte le route di scrittura (PUT /, POST /mail-test) restano admin-only (fail-closed). - Frontend: solo un 401 (fallimento d'autenticazione) distrugge la sessione — nell'arm di redirect a 3 secondi di SettingsPage e nell'arm d'errore useMe di App.tsx. Un 403 ora rende un banner distinto "Non hai i permessi per vedere queste impostazioni." (settings.errorNoPermission, tutte le 8 lingue) e la sessione sopravvive.
  • Ingest del Collector: i file caricati con estensione da testo (.txt .md .csv) ma con magic bytes PDF non sono più indicizzati come UTF-8 grezzo (l'incidente AI-ACT.txt del 2026-10-01). Un PDF rinominato .txt e letto come testo alimentava l'indice vettoriale solo con l'interno non compresso del PDF (dizionario Info, metadati XMP, dizionari di oggetti, elementi di struttura) — 3321 chunk, zero di testo normativo — mentre il corpo nelle stream compresse FlateDecode degradava in spazzatura U+FFFD. parseFile ora annusa l'header %PDF- (primi 1024 byte, finestra a partire dalla spazzatura ISO 32000) sui file dichiarati testo e instrada al parser PDF; i metadati dei chunk guadagnano un timbro routedAsPdf e un falso positivo dell'annusata (un file di testo che meramente menziona "%PDF-" presto) ricade sul parser di testo invece di far fallire l'ingest.
v0.28.1
2026-10-02

Added

  • Connettori: i connettori esistenti ora possono essere MODIFICATI dall'UI admin (Q1-quick) — l'arm PUT /api/connectors/:id pre-esistente finalmente affiora: il menu overflow della scheda ottiene Edit (primo item), che apre un dialog pre-compilato sulla superficie mutabile (nome, binding dell'archivio documentale, messaggi di benvenoto/fallback, fallback locale, rate limit + daily session limit — vuoto = null, 0 = il sentinella UNLIMITED del router). isEnabled resta sulla Switch della scheda, Telegram pollMode sui pulsanti webhook, e piattaforma/botToken/workspace restano immutabili per schema. L'arm PUT ottiene anche l'evento di audit chat_connector.updated (alla pari con l'arm DELETE, D-21).
  • Phase 221 (INSTLIC) — il paste slot della licenza d'istanza (upgrade enterprise senza .env): una nuova superficie admin GET/PUT/DELETE /api/license/instance (requireAdmin + middleware tenant; lo stacking per-route lascia GET /info e /diagnose intatti) e il pannello Settings → Security → License. L'incolla verifica probe-first un JWT RS256 di tier ENTERPRISE non-clamato (una chiave rifiutata non persiste mai: enum chiuso { error, reason } — invalid / expired / plugin_bound / community_tier), conserva SOLO ciphertext AES-256-GCM in SystemConfig (chiave INSTANCE_LICENSE — la disciplina write-seam di SMTP_PASS), invalida la cache Redis D-07, e si applica all'istanza IN CORSA via lo STESSO resolver che il passo di boot usa (setCachedLicense — gli override di limit iniettati dal loader sopravvivono; i flag booleani vanno live immediato; la cancellazione dell'incolla torna allo stato al prossimo boot con parità di boot). Precedenza, boot e runtime identiche: slot → riga plugin verificata → ENV LICENSE_KEY → Community. Il bulk PUT /api/system/settings rifiuta la chiave (la dottrina della route di scrittura dedicata per la retention chat) e l'elenco delle settings non espone mai il ciphertext.
  • Phase 219 (SETP-01/02) — flag view + modify per-ruolo per ogni sezione Settings: RoleSectionVisibility.canModify additivo (sottrae dal fallback SETTINGS_SECTION_MODIFY_PERMISSIONS — mai concedere, admin:settings terminal), settingsModify in GET /api/roles/me/menu-sections, gate server sulle mutazioni di sezione che restituisce 403 { error, section } (le mutazioni /api/providers → llm; POST /api/agency/users → team), rendering client read-only delle sezioni visibili-ma-non-modificabili, e una colonna Modify nell'editor di visibilità dei ruoli (PUT /api/roles/:id/visibility accetta canModify per entrata — lascia-stare quando assente).

Fixed

  • I dialog di conferma-delete della barra laterale non strabordano più (Q5-quick): le conferme delete di chat/cartella erano box w-80 il cui Button del ui-kit è whitespace-nowrap shrink-0 PER DESIGN — le voci lunghe localizzate delle azioni cartella ("Mantieni N chat…" / "Elimina N chat permanentemente") sfuggivano orizzontalmente dal box, e nomi di cartella interpolati lunghi strabordavano il titolo. I box ora sono w-[22rem] max-w-[calc(100vw_-_2rem)] (più larghi + limitati al viewport), le righe di bottoni flex-wrap, le due voci d'azione lunghe overrideano il nowrap (whitespace-normal text-left), e titoli/testi interpolati portano break-words. Le tre chiavi i18n mancanti del dialog delete-chat (sidebar.deleteChat / .deleteChatConfirm / .deleteChatButton) spedite in tutte le 8 lingue — l'UI italiana non mostra più i fallback inline inglesi lì. Q5 follow-up: il primo cap non compilava mai — calc(100vw-2rem) dentro un valore arbitrario è CSS INVALIDO (Tailwind richiede l'idioma underscore calc(100vw_-_2rem) per gli spazi attorno agli operatori) e la regola non era mai emessa silenziosamente, lasciando il box da 352px senza limite sui viewport stretti. L'utilità corretta spedisse nel CSS costruito (max-width:calc(100vw - 2rem) verificato nel bundle dist).
  • Risoluzione del modello: i turni di chat non atterrano più sul default hardcodato qwen2.5:3b quando non esiste un pin esplicito (Q4-quick — la causa radice dell'incidente delle quattro chat: le chat fresche servivano un piccolo modello locale invece del default del workspace, 34-98s per turno). Due correzioni: (1) i default di schema di Chat.model e WorkspaceAgentConfig.model cambiati da "qwen2.5:3b" al sentinella "" (migrazione additiva — ALTER COLUMN … SET DEFAULT, righe esistenti intatte): la catena falsy (model || chat.model || undefined, orchestrator:388/1059) ora scivola sulla catena agent-config del workspace esattamente come la sentinella gemella "default"; (2) la preferenza modello per-workspace nel localStorage ora è SOLO-ESPlicita — le scelte dalla palette scrivono explicit: true, il ri-persist automatico della reseed è sparito (un default risolto per catena non è una scelta utente e non deve diventare sticky), e i valori legacy senza marker sono puliti una volta e mai più usati come candidati. Una preferenza obsoleta (es. qwen2.5:3b, ancora valida-ma-lenta nel registro) non può più ombreggiare il default del workspace a ogni nuova chat.
  • Chat: "un messaggio apre N chat" — il client adottava il chatId del server solo sull'evento SSE done (fino a ~90s più tardi nei turni ricchi di tool), così un retry dell'utente in quella finestra re-inviava senza chatId e il server creava una NUOVA chat per retry, mentre il turno in volo completava in background come riga orfana (Q3-quick). La route di stream ora emette un evento additivo chat con il chatId all'INIZIO del turno (prima di ogni token); il client lo adotta immediatamente (guarded + idempotente — un invio che porta già un chatId mai ri-adotta) e aggiorna subito la barra laterale. I retry ora atterrano nella STESSA chat. Byte-identico per client pre-Q3 (gli eventi SSE sconosciuti sono ignorati).
  • Memory recall: una coppia (user, workspace) con ZERO ricordi conservati ora salta l'intero hook di recall pre-LLM (Q2-quick) — un count Postgres indicizzato (@@index([userId, workspaceId])) barra l'hook prima di qualunque lavoro: niente query embedding, niente round-trip del collector, e niente più avvertimento per-turno [vector-store] Table "ws_user_memory_…" not found da una tabella LanceDB che legittimamente non esiste fino alla PRIMA scrittura di ricordo (l'ensureTable di vectorStore crea alla prima ingest; l'arm di ricerca fail-open su []). Un singhiozzo DB nel count fail-open sul recall pre-sbarra (il contratto best-effort dell'hook — non bloccare mai la chat).
  • shared: la guardia di regressione FEATURE_FLAGS era rimasta indietro per la mossa EE dei connettori (quick 260930-l4q) — pin alzato 12 → 13 per connectors_enabled.

Fixed

  • Backup/restore: un dry-run o un restore che fallisce con "Unable to decrypt (no key in chain matched)" (credenziali di destinazione scritte sotto un precedente ENCRYPTION_KEY) ora restituisce un errore guidato che nomina la causa, i leva di recupero (LEGACY_PREVIOUS_ENCRYPTION_KEYS / il precedente ENCRYPTION_KEY, poi il riavvio), il numero di chiavi provate, e il runbook docs/ENCRYPTION_KEY_ROTATION.md — che ha guadagnato una sezione di recupero Docker "Backup restore dry-run fails". Solo la lunghezza della catena di chiavi è divulgata, mai il materiale delle chiavi.
  • Phase 220 (SPLIT-01/02) — coerenza dello split del repository: docs/REPO_BOUNDARIES.md (matrice di audit modulo→repo per 4 repository con la dottrina di classificazione a 7 criteri e le eccezioni by-design) + la barriera dei confini di repo: scripts/repo-boundary-check.cjs (grep gate con allowlist — il core non deve referenziare i pacchetti plugin privati fuori dalle due nahtstelle di loader e dai siti di contratto documentati) + l'auto-test scripts/repo-boundary-check.test.cjs + il job CI repo-boundary-check cablato in build.needs
v0.25.0
2026-09-09

Added

  • UI: revisione completa della shell (R-2 → R-5). Lo stato vuoto del chat mostra un wordmark monocromatico elegante (l'animazione glitch ciano/magenta resta confinata al tema hacker); la barra laterale diventa una rail minimale in stile Claude (nuova chat + lista chat + menu in fondo) con la navigazione piena in un overlay cmdk a misura di tastiera (Cmd/Ctrl+/) che mantiene l'esatto filtraggio RBAC; il menu utente si è spostato dalla barra superiore a un dialog dedicato (account, lingua, tema, impostazioni, esci) con un drawer a sinistra su mobile; Settings si apre come un grande dialog master-detail (menu a sinistra + dettaglio a tutta area con toggle rail e breadcrumb) invece di una pagina routata — l'area principale ora è il chat da solo su ogni pagina. I temi (light "Pearl", dark, hacker, system) hanno ricevuto una passata di coerenza con correzioni di contrasto WCAG-AA per il testo attenuato e gli accenti chat, guardie prefers-reduced-motion per ogni overlay, e branding consapevole del white-label sul wordmark.
  • Docker: il servizio opzionale di metaricerca searXNG aggiunto allo stack compose (infrastruttura pura — nulla dipende ancora da esso; tag di release fissato, healthcheck via wget, settings auto-provvisionate dall'entrypoint dell'immagine).
  • Docker: il plugin privato @simmetric-chat/saas ora si monta nel server compose come il plugin enterprise (bind mount dell'intero repo gemello; il saasLoader no-ops su MODULE_NOT_FOUND in modalità community).

Fixed

  • Seed: un crash di boot durante il seeding degli utenti poteva spazzare via i ruoli e barrare l'admin; i seed ora si auto-sano — il grant idempotente ri-applica il ruolo admin all'utente admin superstite a ogni boot (ispirato sugli elementi rinviati di STATE.md).
  • Tenancy: POST /api/auth/register ora committa la creazione dell'utente, il grant del ruolo predefinito e l'appartenenza all'org default in un'unica transazione, e ogni percorso di seed/boot ri-esegue l'helper di appartenenza idempotente sul suo ramo "utente già esistente" — un crash a metà boot/richiesta non può più lasciare un utente senza appartenenza (review Phase 182 WR-01); il tombstone-resurrect ora applica il roleInOrg di chi chiama così la rimozione degrada i privilegi (WR-02).
  • Build enterprise: la sezione Settings → Advanced → Backup (e le route /sso e /logs) non resta più barrata dietro la scheda "requires Enterprise" dopo un login in-app o un accesso SSO. La query del manifest dei moduli enterprise (GET /api/enterprise/modules) ora è auth-gated e reattiva alla sessione: prima veniva fetched una volta, senza autetnicare, al boot dell'app e messa in cache per l'intera sessione (retry: false + staleTime: Infinity), così sulla schermata di login dava 401 e le funzioni Enterprise restavano nascoste fino a un ricaricamento completo CON il token conservato. L'hook ora si sottoscrive a useMe() e ri-fetches il manifest quando appare una sessione (login a password, handoff SSO ?token=), e non interroga mai l'endpoint senza sessione.
  • Docker: l'helper di provision-encryption-key.sh di produzione non più crash-loopa il server quando il .env di root viene schierato con un segnaposto del template .env.example non riempito (<sostituire-con-valore-generato>) come valore di API_KEY_HMAC_SECRET/ENCRYPTION_KEY — un segnaposto ora è riconosciuto come "unset" (avviso rumoroso) e la provision cade sui percorsi restore/generazione. Qualsiasi altro valore non-vuoto invalido fallisce comunque rumoroso alla barriera di boot.
  • Security: il router helper E2E (/api/__tests__, server echo MCP start/stop senza autetnicazione) non è più montato quando il server gira con NODE_ENV=production — i boot di produzione ora rispondono 404 lì, applicando il contratto documentato dev/test-only del router (nessun endpoint di spawn-processo senza autentica in produzione).
  • Security: il controllo del callback del collector del import dell'archivio (PUT /api/archives/import/:jobId/callback) ora confronta l'header X-Collector-Secret a tempo costante (crypto.timingSafeEqual con guardia di lunghezza), allineando la disciplina timing-safe già usata dal callback di stato dei documenti e dalla barriera segreto del collector — chiudendo l'ultimo confronto di segreto in chiaro sul server.
  • Auth: POST /api/auth/login ora valida il corpo a livello di route con lo schema condiviso loginSchema (safeParse) — un corpo invalido o vuoto restituisce 400 { error, details } coerente con ogni altra route auth, invece di un 401 con un messaggio Zod nudo. Credenziali valide-ma-sbagliate restituiscono comunque 401 "Invalid credentials".
  • Auth: login ora esegue un dummy bcrypt.compare su un hash a gettare fisso quando l'utente non è trovato, così il tempo di risposta non distingue più usernames esistenti da inesistenti (irrobustimento enumeration). Entrambi i percorsi restituiscono lo stesso 401 "Invalid credentials".
  • Auth: POST /api/auth/admin-reset-password ora valida il proprio corpo con il nuovo schema condiviso adminResetPasswordSchema (userId UUID + newPassword di 8-128 caratteri, safeParse → 400 { error, details }) — l'ultima route di auth lascia la validazione del corpo destrutturata ad-hoc.
  • Auth: il ramo di registrazione chiusa di POST /api/auth/register non duplica più la logica auth inline — usa verifyToken / isTokenRevoked / isAdmin / getCachedUserWithRoles importati staticamente (niente await import dinamico, nessun ri-controllo header morto, lookup utente in cache) preservando ogni forma di risposta e l'ordine revocation-before-lookup.
  • Security: l'endpoint cache-bust del widget (POST /api/config/:widgetId/cache-bust) ora confronta l'header X-Api-Key a tempo costante (crypto.timingSafeEqual con guardia di lunghezza) e fail closed se la chiave attesa non è impostata, al posto del confronto plaintext con forma fail-open expected && — il controllo della chiave del widget ora rispecchia la disciplina timing-safe usata dalle barriere segreto del server e del collector.
  • Pipeline di release: tutte e cinque le immagini Docker sono ora costruite solo amd64. Le build linux/arm64 emulati QEMU restavano appesi fino a ~5h (il widget per primo, poi la compilazione sorgente pgvector all-in-one) e esaurivano il disco del runner sui minuti pagati di un repo privato; setup-qemu rimosso, ogni voce della matrice pubblica linux/amd64, e la matrice di build è de-serializzata a max-parallel: 2. Gli utenti arm64 possono auto-compilare localmente (i Dockerfile sono cross-build-safe) o attaccare un runner arm64 self-hosted — vedi la nota arm64 di docs/DEPLOYMENT.md.

Changed

  • Tooling: una barriera dead-code ora gira in CI (knip) e fallisce su file, dipendenze o export inutilizzati fuori dalle allowlist documentate; una spazzata repo-wide ha tolto ~3400 righe di codice morto, la generazione della API key del widget ora vive dietro il flusso admin standard, e gli script i18n segnalano come hint di pulizia non-bloccante le chiavi di traduzione definite-mai-usate.
  • Widget: il pin del modello di risposta per-widget — la configurazione admin di ogni widget ora può selezionare il provider LLM + il modello che serve le risposte chat di quel widget (responseProviderId/responseModel, migrazione additiva add_widget_response_model). Il pin si risolve server-side dalla riga DB del widget a ogni richiesta chat (mai fornito dal client — il proxy widget e l'UI visitatore sono intatti), ha priorità sul default del workspace, e i widget non impostati mantengono la catena workspace/globale esistente. Le chat widget ora registrano il modello che le serve davvero.
v0.22.0
2026-08-30

Added

  • RAG: il filtro dei metadati per rag_search — l'agente ora può restringere il retrieval per tipi di documento (documentTypes: pdf/md/txt/csv/docx/xlsx) e per date di ingest (dateFrom/dateTo, ISO; un limite superiore a solo data include l'intero giorno), correggendo la modalità di fallimento "documenti del 2023 per domande del 2025". I filtri attraversano la ricerca ibrida entrando in entrambe le gambe di retrieval: la gamba vettoriale raggiunge il collector come veri pre-filtri sui metadati di ingest timbrati (predicati JSONB pgvector + clausole must del payload Qdrant; LanceDB e Chroma ignorano le nuove chiavi con un warn loggato), e la gamba FTS guadagna predicati SQL parametrizzati sulla tabella documents. Un backstop post-retrieval server-side contro la tabella documents autorevole garantisce correttezza filtrata su ogni provider. I documenti da ora in poi sono sempre filtrabili; i vettori legacy (pre-timbratura) sono esclusi mentre un filtro è attivo e tornano filtrabili via re-embed admin — nessun cambio di schema, nessun ri-embedding dei documenti esistenti, e il comportamento senza filtri è invariato.
  • DLP: quattro nuovi pattern built-in per gli identificatori europei (spec. italiani) più comuni, seminati idempotentemente da una migrazione solo-dati e rispecchiati nel set fallback senza DB — it_vat_iva (Partita IVA; ancorata all'etichetta, così un numero nudo di 11 cifre non matcha mai), it_codice_fiscale (codice fiscale a 16 caratteri; solo maiuscolo, parole di prosa come "foschi" non matchano mai), iban (prefisso paese + lunghezza ≥ 15; forme compatta e raggruppata per spazi, tempo lineare/ReDoS-safe), e eu_phone (telefono IT/EU; seminato DISABLED per revisione admin, dato che ogni regex telefonica strutturata flagga sequenze di cifre d'ordine/riferimento). I built-in sono ora 10 in tutto; le installazioni esistenti ottengono le nuove righe su migrate deploy con la sicurezza di replay ON CONFLICT DO NOTHING.
  • Chat: gli admin possono ora rivelare o ri-nascondere i testi censurati DLP su tutta la conversazione in una volta, via un nuovo item "Show DLP texts" / "Hide DLP texts" nel menu more-actions (+) dell'input chat (icne Eye/EyeOff, mostrato solo agli admin visto che solo loro possono espandere un avviso DLP). La preferenza è scoped alla sessione, di default nascosta e si applica a ogni avviso, inclusi i messaggi appena arrivati; il pulsante occhio per-avviso continua a funzionare per le override manuali per-messaggio tra i toggle globali.
  • Configurazione dei pattern DLP: i sei pattern DLP ora sono salvati in database (tabella dlp_patterns, seminata idempotentemente dalla migrazione così le installazioni esistenti ottengono i built-in) invece che hardcodati. Gli admin li gestiscono in Settings → Advanced → DLP Patterns: abilita/disabilita qualsiasi pattern per riga, rinomina i built-in (il loro regex è congelato — i tentativi di modifica restituiscono 400), e aggiunge fino a 50 pattern personalizzati (nome snake_case, sorgente regex con validazione di compilazione al salvataggio, flags, replacement) con una anteprima live del test che mostra i segmenti matchati e il testo censurato. Le scansioni che possono caricare il set DB — l'inlet/outlet del filter plugin e il flush finale di fine risposta — usano i pattern configurati tramite una cache TTL di 5 minuti con caching per-pattern dei regex compilati, ricadendo sul set built-in quando il DB è irraggiungibile; il flush progressivo token-per-token della stream mantiene i pattern built-in (limitazione v1 documentata). L'intero pannello è localizzato in tutte le 8 lingue.
  • Bypass dei ruoli DLP: una nuova impostazione di sistema DLP_BYPASS_ROLES (array JSON di nomi di ruolo, default []) permette agli admin di esentare i ruoli selezionati da TUTTO il scansionamento e censura DLP — l'inlet/outlet del filter plugin e il core del flush progressivo della stream (incluse la scansione del RAG-context e la censura del thinking, sia per il chat JWT che per l'account servizio del widget) onorano tutti la stessa barriera. Ogni run bypassato fa partire un evento audit fire-and-forget dlp.bypassed che registra CHI ha bypassato (i nomi dei ruoli intersecati) e la superficie d'origine — mai il contenuto scansionato. UI admin: un multi-select di ruoli con un avvertimento d'esposizione in Settings → Advanced → DLP, e un'etichetta d'azione "Bypassed" nel pannello DLP Match History, localizzati in tutte le 8 lingue.
  • Tagging e filtro della fonte d'audit DLP: ogni registro di evento dlp.* porta source: "chat" | "widget" nei suoi metadati — il core condiviso della stream SSE deriva il tag dall'header X-Widget-Id del widget così le conversazioni widget sono censurate come quelle del chat ma ora distinguibili nel pannello DLP Match History (Settings → Advanced). Il pannello ottiene filtri client-side per fonte (Tutti/Chat/Widget), ID chat e utente.
  • UI admin per il reaper delle bozze di upload: una nuova sezione in Settings → Maintenance espone il toggle enabled (upload_draft_reaper_enabled) e l'espressione cron di cadenza (upload_draft_reaper_cron) via PUT /api/system/settings, con un pre-controllo client-side della forma cron a cinque campi (il server mantiene il suo comportamento warn-and-fallback per i valori invalidi), toast di successo/errore localizzati e descrizioni in tutte le 8 lingue. .env.example documenta le chiavi del reaper come override env-var (upload_draft_reaper_enabled, upload_draft_reaper_cron, upload_draft_retention_days).
  • Config: la configurazione d'ambiente è ora definita una volta in uno schema Zod condiviso (@simmetric-chat/shared), la precedenza efficace è documentata e test-fissata come DB > ENV > default (le modifiche dalla UI dei settings vincono immediatamente; matrice a 8 casi), e le chiavi che verrebbero ignorate perché una variabile d'ambiente le riempie sopra sono mostrate con un badge envOverridden nella UI dei settings.
  • Config: il .env alla root del repository è ora l'unico config di runtime per server, collector e widget — un nuovo loader a zero dipendenze (loadRootEnv()) lo unisce sotto le override dei .env per-pacchetto (legacy), con un avvertimento di deprecazione una-per-boot che elenca i nomi delle chiavi toccate (mai i valori); Docker e Tauri allineati (il compose env_file punta al file root, nessuna immagine inforna segreti).
  • Config: tutti e tre i .env.example dei pacchetti sono rigenerati dai loro schemi Zod (83/15/6 chiavi, raggruppate, default sincronizzati) con tripwire Jest che falliscono quando una futura chiave di schema resta non documentata; le letture raw di process.env tenute fuori dallo schema (HF/openai-quirks, chiavi encryption/HMAC, LOG_LEVEL, barrieri di test) sono fissate da 44 probe di guardia comportamentali.
  • UX: l'eliminazione di un workspace ora chiede un dialog di conferma standard (era un mini-confirm inline), i workspace supportano il bulk delete multi-selezione con dialog di conferma e un toast dei risultati che conta le righe saltate (le righe senza permesso sono saltate, mai un fallimento silenzioso), l'anteprima OCR affiancata mantiene il testo estratto legato alla stessa porzione di pagina del riquadro immagine, e il dettaglio del run di sintesi mostra il pulsante di approvazione disabilitato con la ragione esplicita sui run non ancora approvabili.
  • UX: le checkbox di selezione workspace e il bulk delete sono localizzati in tutte le 8 lingue.

Changed

  • Il reaper delle bozze di upload è pienamente configurabile via le impostazioni di sistema: un toggle enabled/disabled (upload_draft_reaper_enabled, default "true") e una override di cadenza (upload_draft_reaper_cron, default giornaliero 03:00 UTC), entrambi gestiti via PUT /api/system/settings. Valori cron invalidi loggano un warning e ricadono sulla cadenza default invece di far fallire il boot; il comportamento predefinito è invariato.

Fixed

  • Corretto un crash-loop di boot in produzione Docker dove l'avvio del server moriva per un fallimento di unique-constraint su api_keys.prefix: il seeder della widget API key ora deriva il prefisso di visualizzazione dal digest HMAC e tollera le collisioni di prefisso (ri-verifica per hash di chiave, warn invece di crash), e la generazione della API key riprova con una chiave fresca su collisione di prefisso (massimo 3 tentativi).
  • Le citazioni chat ora riflettono le fonti su cui una risposta si fonda davvero invece di tutto ciò che i tool di ricerca hanno prelevato: dopo la deduplicazione delle citazioni, un filtro di grounding limita le citazioni per documento al top-2 per punteggio di rilevanza e manda via le citazioni di chunk il cui testo non condivide sovrapposizione lessicale significativa con la risposta finale (barriera conservativa — le pagine wiki/archivio, i risultati web/ricordo, le citazioni senza testo di chunk e le risposte molto brevi sono sempre conservate). Le risposte "nessun'altra informazione" non mostrano più "Fonti (5)" di un solo documento a ~1,5% di rilevanza; la diclaimer "Nessuna evidenza sostegno" è valutata allo stesso modo sull'insieme filtrato (sia il percorso streaming che non-streaming). Solo la pipeline delle citazioni — nessun cambiamento di UI, i18n o schema.
  • Docker: il healthcheck del servizio ollama ha sostituito la probe curl (l'immagine ollama/ollama non include curl/wget/node — il container riportava "unhealthy" per sempre) con una probe ollama ls; il healthcheck qdrant ha sostituito la probe wget (image senza wget/busybox) con un controllo socket bash /dev/tcp — entrambi i servizi ora riportano healthy. L'docker/entrypoint.sh del container singolo ora sorga provision-encryption-key.sh (parità con l'immagine separata): i boot standalone di produzione auto-provvigionano ENCRYPTION_KEY + API_KEY_HMAC_SECRET invece di fallire la barriera. Tutte e cinque le Dockerfile lintiscono pulite sotto hadolint (politica unpinned-deps documentata + waiver ENV-segnaposto).
  • Gli eventi di cronologia dell'audit DLP non mostrano più "nessun dettagli della corrispondenza" quando il contenuto incrossato è arrivato in un chunk iniziale della stream: l'evento streaming dlp.output_match ora deriva i SUOI tipi di match E l'elenco dei testi incrossati dai match accumulati su tutta la corsa (tutti i flush progressivi più la scansione finale della coda) invece della sola scansione dell'ultimo chunk — le righe storiche potevano salvare un array matchTypes vuoto mentre la riga mostrava comunque un badge del tipo di match. Il testo incrossato è salvato per il precedente del plugin filtro DLP (dlp.input_match / dlp.output_match non-streaming lo portano già); il pannello audit è solo-admin e barrato Enterprise. La vista espansa del pannello ora rende l'elenco dei tipi rilevati con un suggerimento "solo per tipo" per gli eventi che registrano tipi di match senza match (localizzato in tutte le 8 lingue), mantenendo la caduta "no details" solo per i metadati davvero vuoti.
  • I file caricati restano disponibili per il ri-tento: i callback di stato terminale del collector non cancellano più i file bozza in storage/uploads/drafts/ (il callback di un documento qualunque poteva cancellare il file da cui ogni ri-tento dipende), e /retry//assign si auto-sano ripristinando il file bozza dalla copia OCR persistente (storage/ocr-sources/) quando presente invece di fallire con "non esiste più sul disco".
  • L'eliminazione di una bozza di upload fallita e obsoleta non restituisce più barrato (409) — DELETE /api/uploads/:id ora tratta gli stati di gamba terminale (completed o failed) come cancellabili e rifiuta solo le bozze con una gamba davvero in volo (abilitata non-terminale), e il bulk delete frontend usa lo stesso predicato in-flight dei chip filtro così che ciò che l'UI offre di cancellare è esattamente ciò che il server accetta.
  • Il ri-tento RAG/KB di un upload la cui bozza è scomparsa ora restituisce un chiaro errore di ri-caricamento (400) invece di un falso toast di successo, le gambe collector fallite non cancellano più il file bozza staged da storage/uploads/drafts/ (i ri-tenti futuri restano possibili — il reaper 24h e la route DELETE restano gli unici cancellatori di file bozza), e i toast di ri-tento ora riflettono l'esito per-gamba: una gamba rifiutata su HTTP 200 mostra un toast d'errore localizzato e il conteggio del bulk retry la conta come fallita.
  • Le distribuzioni Docker di produzione auto-provvigionano una ENCRYPTION_KEY persistente (volume server-storage, /app/storage/.encryption-key) invece di crash-loopare sul default duro. L'entrypoint del server genera la chiave una volta prima di ogni passo Prisma, la ripristina a ogni riavvio/ricostruzione, preferisce un valore fornito dall'operatore e fallisce rumoroso su una chiave persistita corrotta. .env.example e docs/DEPLOYMENT.md aggiornati per marcare la variabile Richiesta-in-produzione. Vedi docs/ENCRYPTION_KEY_ROTATION.md per le note Docker.
  • Le pagine OCR della KB non falliscono più quando la stream di visione Ollama viene chiusa dal motore di modello senza il marker done finale dopo una trascrizione completa (osservato con il motore personalizzato glm-ocr): ocrPage ora salva l'output accumulato non vuoto come risultato della pagina con un flag truncated e un log di avviso, invece di scartare una trascrizione reale dietro "Did not receive done or success response in stream." Una stream senza done ma senza contenuto fa comunque fallire la pagina.
  • I riassunti dei job OCR sono veritieri sulla salute delle pagine: le pagine che finiscono con un marcatore [FAILED: sono contate e salvate come failedPages nel JSON del risultato del job, e i log di completamento emettono un avvertimento [ocr] Job completed with failed pages, così un job COMPLETATO con alcune pagine fallite non è più indistinguibile da un successo pulito (zero token + puntatore qualità al minimo da soli erano ambigui).
  • Documentato e corretto la modalità di fallimento per le installazioni Enterprise: quando @simmetric-chat/shared guadagna NUOVI file sorgente, uno snapshot fuori-età della dipendenza file: enterprise può crashare il server al boot con "Cannot find module './env.schema'" — pnpm effettua lo snapshot del pacchetto file: in node_modules/.pnpm e i file creati dopo l'ultimo pnpm install non raggiungono mai quello snapshot (gli hardlink aggiornano solo i file già esistenti), così lo schemas/index.js dello snapshot richiede un modulo che non esiste sul lato enterprise, e la politica fail-loud del loader trasforma il fallimento della registrazione del plugin in un crash-loop di boot. La correzione viaggia come documentazione: il runbook di ricostruzione del repository enterprise ora copre la distinzione reinstalla-non-ricompila (aggiorna lo snapshot con pnpm install, poi pnpm build), e il runbook di installazione air-gap in docs/ENTERPRISE_PLUGIN.md guadagna la sottosezione di troubleshooting corrispondente (sintomo, cause, correzione, controllo di sanità) così anche gli operatori senza il repo privato trovano il rimedio.
v0.21.0
2026-08-28

Sweep dei debiti & igiene di release (era: v1.5.0)

Added

  • docs/LICENSE_DECISION.md — analisi del modello di licenza per la doppia distribuzione (community + enterprise). Decisione: AGPL-3.0 (community) + commerciale proprietaria (enterprise).
  • LICENSE sostituito da Apache-2.0 → GNU AGPL-3.0 (repo community).
  • LICENSE_EE.md — la licenza commerciale proprietaria del plugin enterprise (@simmetric-chat/enterprise).
  • NOTICE — la spiegazione della doppia licenza alla root del repo.
  • CLA.md — l'accordo di licenza del contributore (v1.0) — copyright + grant di brevetto, consapevole della doppia licenza. Richiesto per tutti i contributi esterni.
  • Header di copyright AGPL-3.0 aggiunti a tutti i 981 file sorgente su 5 pacchetti (.ts/.tsx). Applicati via scripts/add-headers.cjs (idempotente).
  • docs/ENTERPRISE_LICENSE_TERMS.md — i termini commerciali/SLA a misura d'operatore per i clienti enterprise.
  • .github/workflows/release.yml — GitHub Release + pubblicazione delle immagini GHCR alla push del tag.
  • CHANGELOG.md (questo file) — formato Keep-a-Changelog, seminato dalla storia dei milestone.
  • .planning/milestones/v1.5-debt-sweep-ROADMAP.md — l'ambito del milestone di valutazione del debito (portato avanti E2E, sessioni di debug, triage dei quick-task, disciplina della sincronizzazione dei timbri di versione).

Changed

  • La package.json version alzò 0.17.0 → 1.4.0 (sincronizzata con l'ultimo tag git v1.4). Le versioni per-pacchetto restano su 0.22.0/0.23.0 (pacchetti privati, non pubblicati su nessun registry).
  • package.json + tutte le packages/*/package.json campo license impostato a AGPL-3.0-or-later.
  • Il badge di licenza del README.md aggiornato da Apache-2.0 → AGPL-3.0; la sezione licenza aggiornata per descrivere il modello a doppia licenza; la sezione Contributing aggiunta con il requisito del CLA.
  • CONTRIBUTING.md aggiornato con le istruzioni di firma del CLA.
v0.20.1
2026-08-27

Scala orizzontale (completamento del layer Redis) (era: v1.4)

Goal: chiudere i 4 elementi rinviati SCALE (CSW-19 + SCALE-02/03/04) e provare lo scaling orizzontale con un vero test di integrazione multi-istanza — abilitando lo schieramento server multi-istanza sicuro dietro un load balancer.

Shipped: 7 fasi (161–167), 13 piani, 29 compiti. Tutti i 22/22 requisiti soddisfatti. Tag git: v1.4.

Added

  • SCALE-01 (CSW-19): gli 3 scheduler rimanenti avvolti con lock → poi tutti gli 8 migrati a pg-boss cron jobs; setInterval + isRunning + withDistributedLock rimossi da tutti gli 8 scheduler migrati.
  • SCALE-02: il fallback scryptSync ENCRYPTION_KEY rimosso in produzione — boot fail-loud (logger.error + process.exit(1)) + lancio di servizio defense-in-depth; dev/test conserva scrypt; runbook di rotazione aggiornato.
  • SCALE-03: la verifica delle API key da loop bcrypt → lookup digest keyed-HMAC O(1) findUnique({key_hash}); migrazione breaking DROP COLUMN hashedKey + ADD COLUMN key_hash; variabile d'ambiente API_KEY_HMAC_SECRET; runbook di migrazione operatore docs/API_KEY_MIGRATION.md.
  • SCALE-04: 8 scheduler setInterval migrati a pg-boss (coda di job su Postgres, amica dell'air-gap); 2 poller a 10s sensibili alla latenza (OCR + sintesi) restano setInterval; singleton pg-boss (jobQueue.ts) con degradazione graziosa.
  • Verifica del fan-out SSE: la rinviata cross-instance è chiusa — 4 test cross-instance su mock che provano il relay SSE Redis pub/sub (A pubblica → B rilancia, skip d'origine, teardown, degradazione).
  • Documentazione operatore: docs/SCALING.md (377 righe) — topologia di schieramento multi-istanza, requisito REDIS_URL, dipendenza Postgres di pg-boss, cross-references SCALE-02/03, poller da 10s, degradazione graziosa.

Modifiche incompatibili — azione operatore richiesta

  • `ENCRYPTION_KEY` deve essere impostata nel .env di produzione (base64, 32 byte) o il server rifiuta il boot. Vedi docs/ENCRYPTION_KEY_ROTATION.md.
  • Le API key passate da bcrypt (apiKeyHash) → HMAC-SHA256 (key_hash). Gli operatori devono ri-emettere le API key widget/collector. Vedi docs/API_KEY_MIGRATION.md.

Infrastructure

  • Nuova dipendenza: pg-boss v12.28.0 (server). Nuova variabile d'ambiente: API_KEY_HMAC_SECRET. Ordine di boot modificato — l'inizializzazione pg-boss è ora il 7º passo async di init con un teardown di shutdown.
v0.20.0
2026-08-26

Spazzata dei CONCERNS (era: v1.3)

Goal: passare l'audit CONCERNS.md del 2026-08-26 — correggere direttamente tutte le vittorie facili, affrontare gli item di media fatica in fasi mirate, e valutare (rinviare o documentare) i punti bloccanti/a grande fatica.

Shipped: 7 fasi (154–160), 14 piani. Milestone di irrobustimento brownfield; nessuna nuova funzione rivolto all'utente.

Added

  • corretto il gitignore della docs, mappa di file di chat.ts, flips delle regole di lint.
  • le vittorie facili di irrobustimento del codice (il cap take:10 di apiKeyMiddleware, ecc.).
  • la riduzione del debito di lint frontend (3 piani, flips warn→error).
  • l'avviso di boot per la ENCRYPTION_KEY in produzione (CSW-11) + la generazione batchata di titolo+tag+followUps.
  • le divisioni di moduli (l'estrazione del parser dei provider, i registri di capacità).
  • la copertura di test (chatImportService, chatExportService, apiKeyService, altri 5 servizi; collector + widget cablati nel config root di jest).
  • valutato e documentato (i diritti JWT di licenza .env, il runbook di rotazione della chiave di firma della licenza, la correzione del claim obsoleto in AGENTS.md).
v0.19.1
2026-08-25

Raffinamenti (era: v1.2)

Goal: branding, disclaimer IA, reimplementazione TypeScript dell'algoritmo di grafo del wiki, correzioni alla ricerca RAG, irrobustimento MCP, primo avvio del Setup Wizard.

Shipped: 6 fasi (149–153 + 153.1).

Added

  • BRAND-01/02: favicon SVG del monogramma S, componente Monogram riutilizzabile, disclaimer IA localizzato e attenuato sotto ogni messaggio dell'assistente (7 lingue).
  • MCP-01/02/03: irrobustimento MCP — Map del trasporto SSE per-sessione, barriera di auth MCP_API_KEY Bearer, chiusa la IDOR di list_workspaces, getMCPToolsForWorkspace cablato in resolveSkillsForChat.
  • RAG-01/02/03: correzione della localizzazione FTS (tokenizzazione multi-lingua per it/ru/de/es/fr/zh), deduplicazione dei risultati in ricaduta rag_search↔wiki_query, prompt di affordance LLM per rag_search vs wiki_query.
  • WIKI-01: la reimplementazione TypeScript dell'algoritmo Graphify del wiki (community detection + generazione markdown del wiki, reimplementazione pulita da Apache-2.0, ~280 LOC Python → TS).
  • WIZ-01/02: l'UI del primo avvio del Setup Wizard (multi-passo: account amministratore + configurazione del provider + DB vettoriale, consumo di /api/system/initialize); SEED_BOOTSTRAP_ADMIN disabilitato quando il wizard è attivo.
  • .1: pulizia del debito tecnico di v1.2 (flip warn→error di React Compiler).
v0.19.0
2026-08-19

Architettura del plugin Enterprise (era: v1.1)

Goal: rifattorizzare il modello di licenza dai "flag di funzionalità runtime nel sorgente pubblico" a un "plugin enterprise privato separato": le funzioni enterprise (SSO, log di audit, white-label, backup) vivono in un pacchetto privato consegnato solo ai clienti paganti; il repo community non contiene codice di funzioni enterprise. Le funzioni commodity diventano sempre-ON nella community.

Shipped: 9 fasi (140–148), 19 piani, 26 compiti.

Added

  • EPA-01: i tipi PluginContext/EnterprisePlugin in shared; la nahtstelle di loader loadEnterprisePlugin(app) / shutdownEnterprisePlugin() con require.resolve→require in due passi (MODULE_NOT_FOUND = no-op della community, altri errori = process.exit(1) fail-loud).
  • EPA-02: 9 flag rimossi da FEATURE_FLAGS (20→10): web_search, webhooks, push_notifications, memory, lead_export, widget_analytics, auto_title, synthesis_rate_limit. Route always-ON.
  • EPA-03: SSO (SAML + OIDC + SCIM 2.0) estratto nel plugin enterprise.
  • EPA-04: log di audit (tabella event_log immutabile + ruolo DB INSERT-only) estratto nel plugin enterprise.
  • EPA-05: branding white-label (hook IoC del validatore di chiave config) estratto nel plugin enterprise.
  • EPA-06: Backup (~1400 LOC: backupService, registro di provider, scheduler Bree, restore, route, UI) estratto nel plugin enterprise.
  • EPA-07: il resolver overrideFeatureLimit con revoca reattiva; il manifest GET /api/enterprise/modules; il caricamento condizionale React.lazy di 4 pannelli enterprise.
  • EPA-08/09/10: priority_support rimosso; custom_agents = limite numerico (community 3, enterprise Infinity); licensePayloadSchema byte-identico (RS256).
  • EPA-12: l'allineamento docs/env (docs/ENTERPRISE_PLUGIN.md, il mount del volume docker-compose, lo split di .env.example), il profilo CI air-gap (grep gate sul servizio di licenza per le primitive HTTP).
v0.18.0
2026-08-12

Candidato public release (simmetric-chat) (era: v1.0)

Goal: preparare e pubblicare il progetto come un repo pubblico pulito, rinominato simmetric-chat, senza segreti/dati personali/percorsi locali, con la verifica delle licenze delle dipendenze, il keygen enterprise (separato, non pubblicato) e la documentazione completa.

Shipped: 8 fasi (132–139), 12 piani, 16 compiti. Il repo pubblico è pubblicato, il rename completo, l'audit delle licenze, il keygen enterprise (HS256 → migrato a RS256 dopo v1.0), i docs canonici, il CI GitHub Actions, il repo fresco.

v0.20
2026-08-11

Widget UX, i18n & Affidabilità

Shipped: 7 fasi (125–131), 32 piani, 75 compiti, 16/16 requisiti soddisfatti.

Added

  • WID-ADMIN: la sottopagina a tab di create/edit del widget (/widgets/:id + /widgets/new), il pannello di preview live, la guardia dirty, la tab leads.
  • WID-I18N: il trasporto a blocchi JSON (uccide il bug %20), la catena di ricaduta visitatore→widget-default→en, la localizzazione di chrome + contenuto a 7 lingue, il threading della localizzazione widget→proxy→server→orchestrator.
  • WID-QUEST: le domande suggerite a tre stati (Default/Nessuno/Personalizzate), gruppi a 7 lingue, la correzione di overflow line-clamp.
  • WID-CREDITS: i crediti in fondo + il badge IA, la rimozione barrata white-label, il relay del popup real-embed verificato.
  • WID-BUGS: i pulsanti d'input da 44px, il round-trip dell'host-FAB, il contratto di icone solo-SVG, il banner contatti, il ciclo wiki, la persistenza dei lead, l'i18n del RAG degradato.
v0.19
2026-08-08

Diagnostica & Debito

Shipped: 5 fasi (120–124). La diagnostica della licenza (log di boot, endpoint diagnose solo-admin, script pnpm license:check), il debito di test (drift ESM, LoginPage QueryClientProvider, spec della forma dei fixture), la valutazione del debito tecnico (spike sul layer di scala Redis — rinviato a v1.4).

v0.18
2026-08-04

Stabilizzazione

Shipped: 4 fasi (116–119).

v0.17
2026-08-02

Enterprise & Rifiniture

Shipped: 8 fasi (108–115).

I milestone precedenti (v0.1–v0.16) sono documentati in .planning/PROJECT.md (sezione Milestone History). L'intera storia a livello di fase vive in .planning/MILESTONES.md.