Registro modifiche
Tutte le versioni di Simmetric Chat, dalla più recente alla più datata.
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) + tettousers.widgetQuotaLimitper proprietario; NULL/fail-closed 0 — le installazioni intoccate vedono zero cambi di comportamento (l'idioma latticemaxSponsoredUsersdi Phase-206). - Widget CRUD ri-armato per PROPRIETARI non-admin:POST /api/widgetsusa il nuovo permessowidget: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: conWORKSPACE_ROLE_ENFORCEMENT=trueuna 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 proprietarioWorkspaceAccessDialoge il pannello SettingsSettingsWorkspaceAccess) 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 surag_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 rimuoverag_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 endpointGET /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 viaattachmentPending) fino a "completed", e lascia cadere l'allegato con un toast sufailed/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 oggettouserincluso (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 campousermai 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 èdisabledfinché un progetto non è selezionato, filtrata perprojectId === selectedProjectId— mentre la pagina Workspaces elencava il workspace condiviso (l'OR-filter diGET /api/workspacesinclude l'arm di grantaccessGrants.some(userId)) ma le righe erano solo di gestione, senza modo di entrare.WorkspaceRowottiene 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("/") — persistelastWorkspaceId, 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 solocurrentWorkspaceId. 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 daGET /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 condivisoworkspaceSelectPlaceholderKeyinsidebar/navModel.tsx(una sentinella vuota obsoleta da una condivisione revocata degrada alla formulazione generica invece di uno scope condiviso abilitato-e-vuoto;workspaceSelectDisabledbarriera i mount diAppNavOverlay— l'inlineAppSidebarNavsi rende sui soli host items). Chiavi i18nworkspace.open+sidebar.selectSharedWorkspace+sidebar.sharedProjectsaggiunte a tutte le 8 lingue — parità verde. Nuovi pin: le righe viewer mostrano Open e mai delete/edit, unonOpenassente non rende alcun controllo, il pin ride-along di pagina asserisce la scritturalastWorkspaceIdnel 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
DialogContentdel ui-kit ègridSENZAgrid-template-columnsesplicito, quindi i figli sedevano su UNA traccia implicitaautoche 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 aggiungebreak-wordsall titolo interpolato +flex-wrapalle 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 nuovoWorkspaceAccessDialog.test.tsxfissa 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 opzionaleusermorto — ogni riga di utente assegnato ricadeva sull'id UUID. Le righe di grant ora risolvono user incluso (forma legacy) →username→ id;WorkspaceAccessGrantottiene il campousernameche 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 marcadata-active).e2e/settings-left-menu-nav.spec.tsè di nuovo verde. - Anche verificato non-un-bug:
POST/DELETE /api/system/settings/branding/iconsono 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-12whiteLabelExtractionGuard); 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 permessoadmin: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 diSettingsPagesuGET /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/settingsora è permission-aware. Gli amministratori (portatori diadmin:settings) mantengono l'intero payload console byte-identico al prima; ogni altro utente autenticato riceve solo il sottoinsiemeNON_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_UPLOADper la pagina uploads) — ogni vocereadOnly: 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 diSettingsPagee nell'arm d'erroreuseMediApp.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.parseFileora 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 timbroroutedAsPdfe 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.
Added
- Connettori: i connettori esistenti ora possono essere MODIFICATI dall'UI admin (Q1-quick) — l'arm
PUT /api/connectors/:idpre-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).isEnabledresta sulla Switch della scheda, TelegrampollModesui pulsanti webhook, e piattaforma/botToken/workspace restano immutabili per schema. L'arm PUT ottiene anche l'evento di auditchat_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 (chiaveINSTANCE_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 → ENVLICENSE_KEY→ Community. Il bulkPUT /api/system/settingsrifiuta 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.canModifyadditivo (sottrae dal fallbackSETTINGS_SECTION_MODIFY_PERMISSIONS— mai concedere,admin:settingsterminal),settingsModifyinGET /api/roles/me/menu-sections, gate server sulle mutazioni di sezione che restituisce403 { 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/visibilityaccettacanModifyper 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-80il cui Button del ui-kit èwhitespace-nowrap shrink-0PER 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 sonow-[22rem] max-w-[calc(100vw_-_2rem)](più larghi + limitati al viewport), le righe di bottoniflex-wrap, le due voci d'azione lunghe overrideano il nowrap (whitespace-normal text-left), e titoli/testi interpolati portanobreak-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 underscorecalc(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:3bquando 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 diChat.modeleWorkspaceAgentConfig.modelcambiati 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 scrivonoexplicit: 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 additivochatcon 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 foundda una tabella LanceDB che legittimamente non esiste fino alla PRIMA scrittura di ricordo (l'ensureTabledi 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_FLAGSera rimasta indietro per la mossa EE dei connettori (quick 260930-l4q) — pin alzato 12 → 13 perconnectors_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 precedenteENCRYPTION_KEY, poi il riavvio), il numero di chiavi provate, e il runbookdocs/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-testscripts/repo-boundary-check.test.cjs+ il job CIrepo-boundary-checkcablato inbuild.needs
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-motionper 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/saasora si monta nel server compose come il plugin enterprise (bind mount dell'intero repo gemello; il saasLoader no-ops suMODULE_NOT_FOUNDin 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
adminsuperstite a ogni boot (ispirato sugli elementi rinviati di STATE.md). - Tenancy:
POST /api/auth/registerora 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 ilroleInOrgdi chi chiama così la rimozione degrada i privilegi (WR-02). - Build enterprise: la sezione Settings → Advanced → Backup (e le route
/ssoe/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 auseMe()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
.envdi root viene schierato con un segnaposto del template.env.examplenon riempito (<sostituire-con-valore-generato>) come valore diAPI_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 conNODE_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'headerX-Collector-Secreta tempo costante (crypto.timingSafeEqualcon 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/loginora valida il corpo a livello di route con lo schema condivisologinSchema(safeParse) — un corpo invalido o vuoto restituisce400 { error, details }coerente con ogni altra route auth, invece di un401con un messaggio Zod nudo. Credenziali valide-ma-sbagliate restituiscono comunque401 "Invalid credentials". - Auth:
loginora esegue un dummybcrypt.comparesu 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 stesso401 "Invalid credentials". - Auth:
POST /api/auth/admin-reset-passwordora valida il proprio corpo con il nuovo schema condivisoadminResetPasswordSchema(userIdUUID +newPassworddi 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/registernon duplica più la logica auth inline — usaverifyToken/isTokenRevoked/isAdmin/getCachedUserWithRolesimportati staticamente (nienteawait importdinamico, 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'headerX-Api-Keya tempo costante (crypto.timingSafeEqualcon guardia di lunghezza) e fail closed se la chiave attesa non è impostata, al posto del confronto plaintext con forma fail-openexpected &&— 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/arm64emulati 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-qemurimosso, ogni voce della matrice pubblicalinux/amd64, e la matrice di build è de-serializzata amax-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 didocs/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 additivaadd_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.
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), eeu_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 sumigrate deploycon la sicurezza di replayON 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-forgetdlp.bypassedche 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.*portasource: "chat" | "widget"nei suoi metadati — il core condiviso della stream SSE deriva il tag dall'headerX-Widget-Iddel 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) viaPUT /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.exampledocumenta 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 badgeenvOverriddennella UI dei settings. - Config: il
.envalla 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.envper-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 composeenv_filepunta al file root, nessuna immagine inforna segreti). - Config: tutti e tre i
.env.exampledei 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 diprocess.envtenute 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 viaPUT /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'immagineollama/ollamanon include curl/wget/node — il container riportava "unhealthy" per sempre) con una probeollama ls; il healthcheck qdrant ha sostituito la probewget(image senza wget/busybox) con un controllo socket bash/dev/tcp— entrambi i servizi ora riportanohealthy. L'docker/entrypoint.shdel container singolo ora sorgaprovision-encryption-key.sh(parità con l'immagine separata): i boot standalone di produzione auto-provvigionanoENCRYPTION_KEY+API_KEY_HMAC_SECRETinvece 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_matchora 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 arraymatchTypesvuoto 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_matchnon-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//assignsi 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/:idora 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_KEYpersistente (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.exampleedocs/DEPLOYMENT.mdaggiornati per marcare la variabile Richiesta-in-produzione. Vedidocs/ENCRYPTION_KEY_ROTATION.mdper 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
donefinale dopo una trascrizione completa (osservato con il motore personalizzato glm-ocr):ocrPageora salva l'output accumulato non vuoto come risultato della pagina con un flagtruncatede 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 comefailedPagesnel 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/sharedguadagna NUOVI file sorgente, uno snapshot fuori-età della dipendenzafile:enterprise può crashare il server al boot con "Cannot find module './env.schema'" — pnpm effettua lo snapshot del pacchettofile:innode_modules/.pnpme i file creati dopo l'ultimopnpm installnon raggiungono mai quello snapshot (gli hardlink aggiornano solo i file già esistenti), così loschemas/index.jsdello 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 conpnpm install, poipnpm build), e il runbook di installazione air-gap indocs/ENTERPRISE_PLUGIN.mdguadagna la sottosezione di troubleshooting corrispondente (sintomo, cause, correzione, controllo di sanità) così anche gli operatori senza il repo privato trovano il rimedio.
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).LICENSEsostituito 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 viascripts/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.jsonversion alzò0.17.0→1.4.0(sincronizzata con l'ultimo tag gitv1.4). Le versioni per-pacchetto restano su0.22.0/0.23.0(pacchetti privati, non pubblicati su nessun registry). package.json+ tutte lepackages/*/package.jsoncampolicenseimpostato aAGPL-3.0-or-later.- Il badge di licenza del
README.mdaggiornato 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.mdaggiornato con le istruzioni di firma del CLA.
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+withDistributedLockrimossi da tutti gli 8 scheduler migrati. - SCALE-02: il fallback
scryptSyncENCRYPTION_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 breakingDROP COLUMN hashedKey+ADD COLUMN key_hash; variabile d'ambienteAPI_KEY_HMAC_SECRET; runbook di migrazione operatoredocs/API_KEY_MIGRATION.md. - SCALE-04: 8 scheduler
setIntervalmigrati a pg-boss (coda di job su Postgres, amica dell'air-gap); 2 poller a 10s sensibili alla latenza (OCR + sintesi) restanosetInterval; 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
.envdi produzione (base64, 32 byte) o il server rifiuta il boot. Vedidocs/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. Vedidocs/API_KEY_MIGRATION.md.
Infrastructure
- Nuova dipendenza:
pg-bossv12.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.
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).
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_KEYBearer, chiusa la IDOR di list_workspaces,getMCPToolsForWorkspacecablato 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).
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/EnterprisePluginin shared; la nahtstelle di loaderloadEnterprisePlugin(app)/shutdownEnterprisePlugin()conrequire.resolve→requirein 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_logimmutabile + 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
overrideFeatureLimitcon revoca reattiva; il manifestGET /api/enterprise/modules; il caricamento condizionaleReact.lazydi 4 pannelli enterprise. - EPA-08/09/10:
priority_supportrimosso;custom_agents= limite numerico (community 3, enterprise Infinity);licensePayloadSchemabyte-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).
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.
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.
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).
Stabilizzazione
Shipped: 4 fasi (116–119).
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.