Durcissement sécurité, fiabilité & polissage du chat#16
Merged
Conversation
Composant réutilisable <ModuleHelp> : petit bouton (i) près du titre d'un module ouvrant un popover (aide contextuelle courte + lien « En savoir plus » vers la doc publique). URL surchargeable via NEXT_PUBLIC_DOCS_URL. Posé sur Dashboard (prise en main), Settings → Providers, Settings → Connecteurs, Documents et l'écran « nouvelle conversation » du chat. Ajoute deux guides utilisateur : prise en main (onboarding 5 étapes) et travailler par projet. Le bouton de la page Projets suivra (fichier en cours de refonte).
Systematic audit-driven fixes across 7 module groups (UI surface), verified with tsc + lint + production build (all green). Criticals: - Chat: scope the streaming live-region (was re-announcing every token) - Board: gate infinite edge animation + node pulse behind reduced-motion; add text/icon status badges (was color-only) - Settings: drop invalid CutoutCard role="button" wrapping nested interactives; add explicit "Configurer" trigger + hoist delete dialog Systemic: - aria-current on active nav (sidebar / settings / admin) - Spinner primitive: role=status on wrapper, sr-only label, motion-safe - form helper text wired via aria-describedby - citation highlight -> --highlight token (was hardcoded yellow) - list/table semantics: ul/li, scope, caption, role=progressbar, dl metrics - reduced-motion gating on spring/infinite animations - dead "Bientot" controls removed; emoji -> @tabler icons - print footer contrast; admin stats no longer hidden on mobile Bugs: - broken oklch(oklch()) vignette gradient (rendered nothing) -> color-mix - hardcoded black MiniMap maskColor -> token (dark-mode correct) Perf: - React.memo on assistant markdown rows via stable onOpenDoc, so historical messages stop re-rendering/re-linkifying on every streamed token
Choix du dossier de stockage à la création d'un projet ; en chat-projet le RAG ne voit que les documents du sous-arbre du dossier + l'historique des conversations du projet. Table message_chunks (embeddings 1024d, index hnsw), projects.folderId, lib/projects/scope. Note: le câblage chat/orchestrateur/outils qui consomme ce scope (route.ts, orchestrator, tools.ts, upload) est intriqué avec le sprint prod-ready et committé avec les phases P1/P3 sur ces fichiers partagés.
…y P1) - R1 usage réel agrégé par message (somme tous agents) + message-metadata émise → pill coût, page usage, quota et URL ?id= fonctionnels - R2 abortSignal req.signal → streamText : « Stop » coupe le pipeline serveur ; withRetry ignore AbortError ; pas de sauvegarde de réponse partielle - H8 estimation coût/appels au point de dépense (composer + CTA Essayer), helper estimateCalls/estimateRunCost centralisé, mode-bar refactorée - H9 agent_runs rattachés au message (messageId) avec modelId/providerType par agent - H22 quota mensuel visible au membre (page usage + dashboard) via getMonthlySpendCents partagé avec l'enforcement - tests cost-estimate + non-retry AbortError (vitest 77 → 91)
- R9 $count scopé userId/projet : plus de fuite cross-tenant de search_documents - R7 purge des chunks des versions périmées au remplacement : l'IA ne cite plus de texte obsolète - H17 détection PDF scanné → extractionStatus=failed + message OCR clair - R6 correction des copies « RAG arrive en v0.3 » (le RAG est en production)
…eady P3 R6) - badge par document : indexé (N segments) / non indexé / clé Mistral manquante - action « Réindexer » par document (recovery après ajout de clé ou échec) - bouton « Réindexer tout » dans l'en-tête - helper réutilisable reindexDocument (ownership, idempotent)
…t projet réel, cascade (P3 🟠) - H16 bouton Importer multi-fichiers + rejets drag-drop (type/taille) surfacés au lieu d'être ignorés silencieusement - H18 « Déplacer vers projet » déplace réellement le document dans le dossier du projet (entre dans le périmètre RAG), plus seulement le projectId d'affichage - H20 le dialog de suppression de dossier avertit de la cascade des sous-dossiers (compte récursif)
…ady P2 R3) Les résultats legifrance_search / pappers_search / pappers_get s'affichent en cartes citation cliquables (titre + source + lien externe, rel=noopener) au lieu d'une pill grise « Terminé » qui jetait les URLs. Re-rendu à l'identique au reload (les tool-results sont déjà persistés). Tient la promesse « Louis cite ses sources ». Sécurité (défense en profondeur) : les URLs proviennent d'API externes (PISTE/Pappers) → validation du schéma http(s) via safeHttpUrl avant tout href ; fallback non-cliquable sinon (jamais de javascript:/data: dans un href).
…3a/H3b/H5) - H3a persiste les data parts du trail (agent-event/output/retry + skills) dans messages.parts → le théâtre, les badges d'étapes et les compétences survivent au reload ; output capé à 12k/agent - H3b getConversationAuditTrail (ownership) : runs groupés par message (messageId rattaché via P1) - H5 export du trail en JSON (par message : agents, rôles, modèles, tokens, latence, statut, erreurs) depuis le menu de conversation
Consomme la part data-skills-detected (live + persistée via H3a) et affiche des pills « Compétences appliquées : X » au-dessus du composer. Mapping slug→libellé fourni par page.tsx (getEnabledSkills). Rien affiché si aucune skill détectée.
…cées, notif de fin (P4) - H14 export CSV (route GET ownership-checkée, BOM UTF-8 + séparateur ; + échappement, Content-Disposition attachment) + item « Exporter en CSV » dans le menu - H15-e le format de colonne (date/money/boolean/liste) est désormais injecté dans la consigne d'extraction - H15-f les lignes « running » abandonnées (after() interrompu) au-delà de 5 min sont requalifiées et redeviennent relançables - H15-d toast « Extraction terminée » (transition running→fini, une seule fois, sans faux positif au chargement) + annonce aria-live
…nts (P4 H15-a/b/c) - H15-a rerunReviewRow : ré-extraire une ligne (toutes colonnes) depuis la grille (desktop + mobile) - H15-b rerunReviewColumn : « Ré-extraire » dans le popover colonne après édition du prompt ; extractRow merge désormais (préserve les autres colonnes) - H15-c addReviewDocuments + dialog : ajouter des documents à une analyse existante (docs indexables pas déjà présents), promesse « en ajouter plus tard » tenue
- R4 PISTE n'annonce comme « débloqué » que Légifrance ; Judilibre/JADE/INPI/BODACC déclassés en « à venir » (carte + dialog) - H23 la bibliothèque de modèles affiche le prix entrée/sortie par M de tokens (« prix inconnu » à défaut, jamais de faux gratuit)
…elf-host (P5 R9'/R5) - R9' checklist de mise en route STATEFUL (provider→modèle→connecteur opt.→chat), reflète l'état réel ; remplace la bannière basée sur le nombre de conversations - R5 (providers) « Tester la connexion » est activé dès qu'un baseUrl est configuré (Scaleway/OVH/Albert/OpenAI-compatible self-host), plus de menu grisé muet — l'action passait déjà le baseUrl de la clé
Action testConnectorKey + fonctions testPisteConnection (OAuth) / testPappersConnection (requête minimale). Item « Tester la connexion » dans la carte connecteur + toast (connecté / auth refusée / non configuré / injoignable) + badge « Dernier test » persistant (lastTestStatus). Comble le trou : un cabinet entrait ses identifiants PISTE sans aucun feedback jusqu'ici.
…/H24) - H26 OVH ne renvoie plus une erreur 501 dans la bibliothèque : retourne la liste curée locale (plus de cul-de-sac), hint « liste curée » - H24 un serveur MCP est synchronisé automatiquement à la création (best-effort, n'échoue pas la création) + la carte affiche la liste nominative des outils découverts, pas seulement le compte
…alette Cmd+K (P7 R8/H27) - R8 ajoute les filets manquants : skeleton de chargement (motion-safe), error boundary brandée avec reset, 404 FR brandée, global-error (use client + html/body, styles inline). Plus d'écran Next brut. - H27 DialogTitle/Description déplacés DANS DialogContent → la palette Cmd+K a un nom accessible (aria-labelledby) et ne déclenche plus le warning Radix
… /board dans la palette (P7 VOCAB/H29) - Décision #8 : libellés UI seulement (sidebar, palette, titres de page, menus composer, panel live, copies models). Routes /board et /workflows inchangées (pas de lien cassé). - H29 (partiel) : Board ajouté au groupe Navigation de la palette Cmd+K.
Décision #6 : DraftingAgent (génère/édite actes & mémoires) et LegifranceAgent (sourcing Légifrance) sont implémentés (prompts + allowlist d'outils) et enregistrés dans AGENT_REGISTRY. Les rôles drafting/legifrance sont désormais proposables dans l'add-dialog (plus de retombée silencieuse sur DefaultAgent). Test du registre mis à jour.
…équentiel only (P7 H7) Les poignées de connexion (qui ne connectent rien, nodesConnectable=false) sont rendues invisibles/non-interactives (conservées pour l'ancrage des edges). Le drag ne ré-ordonne plus qu'en mode séquentiel — en council/parallel la position n'a aucun effet sur l'exécution, le drag y était trompeur. Reste (H7) : vue-liste verticale sur mobile (décision #5).
… export CSV/JSON (P7 H21) - labels d'action centralisés (lib/audit/labels) et réutilisés sur la page audit ET la fiche utilisateur (plus de slug brut) - filtres action + plage de dates + réinitialisation, pagination (50/page), total réel (plus de cap dur à 200) - colonne meta (IP/user-agent/motif…) désormais rendue - export CSV (BOM + anti-injection) et JSON via /api/admin/audit/export (requireAdmin catché → 403, pas 500 ; filtres respectés)
…le=status) (P7 H28/A11Y) - harmonise les focus-ring des inputs/boutons custom sur ring-3 (cohérent avec les primitives shadcn) — 9 occurrences - le feedback transitoire de la fiche utilisateur admin passe en role=status aria-live=polite (annoncé aux lecteurs d'écran)
Remplace le champ texte libre (où une typo donnait un agent sans outil, silencieusement) par un sélecteur : « Tous les outils » (null) vs « Sélection » (cases à cocher des outils réellement disponibles — connecteurs actifs + génération docs + RAG + MCP synchronisés, calculés côté serveur). Un outil d'une allowlist héritée mais indisponible est listé et signalé « indisponible ». « Aucun » = sélection vide.
- src/lib/format/time.ts : formatRelativeFr unique (avant : deux copies divergentes dashboard/admin), nullLabel paramétrable. - src/components/empty-state.tsx : EmptyState réutilisable (carte pointillée + titre + corps + action), uniformise les traitements ad hoc. - Câblage : dashboard, admin/users, admin/audit, workflows ; suppression des fonctions/JSX locaux dupliqués.
Les labels à text-[9px] cumulaient une taille minuscule ET un affaiblissement de contraste (opacity-70 sur du muted-foreground hérité, text-foreground/50, text-destructive/70) → sous le seuil WCAG AA 4.5:1. - admin/users : StatCell + « Ce mois » → 10px, muted-foreground franc (suppression du double-mute opacity-70). - chat/model-picker : label souveraineté → 10px, foreground/70. - chat/chat-shell : labels Avant/Après du diff → 10px, couleur pleine.
Les switches actif/inactif (connecteur, provider, MCP) appelaient l'action sans await ni vérification : un échec serveur (ligne disparue, erreur DB) laissait le toggle muet — l'utilisateur croyait avoir activé une intégration qui restait inactive. - Les trois toggle*Active renvoient désormais ActionResult (erreur si introuvable, try/catch sur l'update) au lieu de void. - Côté client : await + toast.error(message) en cas d'échec. Le Switch étant piloté par l'état serveur, il ne bascule que sur succès+revalidate (pas de faux positif optimiste à annuler).
La barre latérale et la palette de commandes maintenaient deux listes parallèles des mêmes destinations — chaque renommage VOCAB (« Bureau » → « Board », « Workflows » → « Trames »…) devait être répliqué et finissait par diverger en libellé, ordre et icône. - src/lib/navigation.ts : PRIMARY_NAV (source unique) + type NavItem. - sidebar-content : consomme PRIMARY_NAV (suppression du tableau local et des 7 imports d'icônes devenus inutiles). - command-palette : PAGES = [...PRIMARY_NAV, ...SETTINGS_PAGES] ; les réglages granulaires propres à la palette restent locaux. - mobile-nav réutilisait déjà SidebarContent — couvert sans changement.
Diff ligne-à-ligne du texte extrait entre une version antérieure et la version courante, accessible depuis l'historique d'un document. - src/lib/diff/line-diff.ts : LCS maison (sans dépendance) avec trim préfixe/suffixe commun, plafond DP (bloc remplacé au-delà), repli des plages identiques en marqueurs « gap ». 12 tests unitaires. - getDocumentVersionDiff : action sécurisée (même propriétaire + même famille de versions), payload borné (MAX_DIFF_OPS). - version-diff-dialog : bouton « Comparer » par version antérieure + rendu vert/rouge avec compteur +/− et bannière de troncature.
Trois manques du panneau live d'un conseil multi-agents : 1. Fallback synthèse — si le synthétiseur échoue, l'orchestrateur émettait emitError + throw : l'utilisateur perdait TOUTE la délibération. Désormais on sert les positions brutes (avec avertissement « non arbitrées / non vérifiées ») via de vraies parts texte — la seule voie effectivement rendue ET persistée par route.ts (data-final-text n'est consommé nulle part). Couvre council ET parallel. Test unitaire : synthé qui throw → pas de throw, texte de repli non vide contenant les positions. 2. Conscience des tours — le panneau affiche « Tour N/M » en council multi-tours (round déjà présent dans les events, désormais consommé). 3. Retry reflété — une carte d'agent relancé passe en « nouvelle tentative N… » dans le panneau flottant (déjà le cas dans le badge).⚠️ Vérifié statiquement (tsc/lint/tests/build + test orchestrateur du fallback). Le rendu SSE live (texte de repli, libellé de tour qui avance en cours de run) reste à confirmer sur un run réel.
Avant : decrypt() levait l'erreur crypto brute sur tout échec (clé ENCRYPTION_KEY
changée, donnée altérée) ; les appelants propageaient un 500 opaque, et un seul
secret corrompu pouvait casser tout un flux.
- decrypt() lève désormais une DecryptError typée et explicite (config
ENCRYPTION_KEY manquante reste distincte) ;
- tryDecrypt() : variante non-throwing { ok, value | error } pour les boucles
multi-secrets ;
- live-catalog : une clé indéchiffrable remonte une LiveCatalogError actionnable
(« re-saisissez la clé ») au lieu d'un crash crypto.
… MCP) Sans contrôle, un membre pouvait faire interroger par le serveur une cible interne (endpoint de métadonnées cloud 169.254.169.254, panel d'admin LAN…) et exfiltrer le résultat via le modèle. assertSafeUrl (lib/net-guard.ts) : impose http(s) et bloque TOUJOURS le link-local / métadonnées cloud (169.254/16, fe80::/10, metadata.google.internal). Posture auto-hébergement : localhost et le LAN (RFC1918) restent autorisés par défaut (Ollama/vLLM locaux) ; LOUIS_SSRF_STRICT=1 les bloque pour les déploiements mutualisés. Appliquée à la création/màj de clé provider et au transport MCP. Limite connue (commentée) : contrôle de l'hôte littéral, pas de pinning DNS.
…es tool Avant : l'historique complet était renvoyé verbatim à streamText. Sur un endpoint souverain à petit contexte (Albert/Etalab, OVH, openai_compatible auto-hébergé), un long fil juridique dépassait la fenêtre et faisait échouer l'appel EN PLEINE délibération. Nouveau lib/orchestrator/context-budget.ts : rogne les messages les plus anciens au-delà d'un budget (LOUIS_CONTEXT_BUDGET_TOKENS, défaut 100k — ne touche quasi jamais les modèles hébergés ; l'exploitant d'un petit modèle local le baisse), en gardant toujours les 2 derniers messages, puis sanitize les résultats d'outils orphelins (sinon 400 provider sur paire tool dépariée). Appliqué dans default.ts et base.ts après l'injection non-fiable.
…stème Les agents juridiques renvoient un long prompt système + un gros schéma d'outils IDENTIQUES à chaque tour et à chaque round de council, sans aucun cache. applyCachedSystem (lib/orchestrator/provider-tuning.ts) : pour une clé Anthropic, déplace le système dans un message `system` porteur d'un breakpoint cacheControl éphémère → Anthropic met en cache le préfixe stable outils+système (~90% de coût input en moins dessus + latence réduite, allège le quota par cabinet). No-op pour les autres providers ou un préfixe trop court (< seuil de cache Anthropic).
extract.ts tronque les très gros documents (extractionStatus="truncated") mais le modèle n'en savait rien et répondait avec assurance sur un contrat à moitié lu. On ajoute désormais une notice dans le bloc du document tronqué l'invitant à déférer à search_documents (RAG) pour le reste.
Avant : embed.ts hard-codait mistral-embed et envoyait CHAQUE chunk de CHAQUE document confidentiel à l'API cloud de Mistral — en contradiction directe avec le positionnement souverain de Louis. resolveEmbeddingModel route désormais les embeddings vers un endpoint OpenAI-compatible auto-hébergé (Ollama/vLLM/TEI) dès que LOUIS_EMBEDDING_BASE_URL est défini (modèle via LOUIS_EMBEDDING_MODEL, clé optionnelle via LOUIS_EMBEDDING_API_KEY) : les chunks ne quittent plus l'infra du cabinet. Repli inchangé sur mistral-embed + clé utilisateur sinon. Le modèle self-hosté doit produire des vecteurs 1024-dim (EMBEDDING_DIM) — documenté.
…n mot) L'overlap entre chunks reprenait les 100 derniers caractères bruts (buffer.slice), ce qui produisait un fragment en milieu de mot en tête du chunk suivant — moins bon pour le retrieval des clauses à cheval sur une frontière. sentenceTail reprend désormais les dernières PHRASES complètes dans le budget d'overlap. Tests ajoutés.
…tion gracieuse
La recherche était du cosine pur, aveugle aux tokens EXACTS qui dominent les
requêtes juridiques (n° d'article, n° de pourvoi, nom de partie, terme défini) :
« la clause à l'article 8 » ne remontait pas forcément le chunk contenant
littéralement « article 8 ».
- Index GIN d'expression to_tsvector('french', content) sur document_chunks et
message_chunks (migration 0007 + déclaré dans le schéma drizzle) ;
- ragSearch / searchProjectMessages fusionnent désormais 0.7 vecteur + 0.3 mot-clé
(3×limit candidats par voie : HNSW pour le vecteur, GIN pour le mot-clé) ;
- dégradation gracieuse : sans backend d'embedding (NoEmbeddingProviderError), on
bascule en recherche mot-clé pure au lieu de ne rien retourner — durcit les
déploiements air-gapped.
100% in-Postgres. Validé contre la DB live (le doc « clause non concurrence »
remonte 1er sur la requête « clause » grâce au boost mot-clé).
…ives Aucun mécanisme de minimisation des données n'existait. Nouvelle route /api/cron/retention, déclenchée par un planificateur EXTERNE (conteneur cron / k8s CronJob / tâche Scaleway — jamais une boucle in-process qui tournerait par réplica) : - gardée par un secret partagé (header x-cron-secret == CRON_SECRET ; sans secret configuré, route inerte 503) ; - single-flight via verrou Redis ; - purge les conversations INACTIVES au-delà de cabinet_settings.retention_days (null = désactivé), en épargnant les conversations ÉPINGLÉES ; cascade FK → messages + message_chunks ; - DOCUMENTS (pièces/preuves) et JOURNAL D'AUDIT volontairement NON purgés ; - chaque purge tracée dans l'audit (suppression prouvable). Validé contre la DB live (vieille non-épinglée supprimée ; épinglée + récente conservées). Migration 0008 + vars documentées dans .env.example.
Avant : un PDF sans couche texte levait ScannedPdfError → document « extraction échouée », non indexé, invisible au RAG et à l'analyse tabulaire. Or une grande part des pièces juridiques françaises sont scannées (assignations, jugements signifiés, contrats manuscrits, PV d'huissier). Nouveau lib/ocr.ts : OCR via l'API Mistral OCR (provider souverain déjà intégré). Le pipeline d'upload, sur ScannedPdfError, tente l'OCR ; en cas de succès le texte est indexé normalement (chunking + embeddings) et le statut passe à « ocr ». Sans clé Mistral, repli propre sur le statut « failed » avec message explicite. UI : badge « OCR » + document traité comme indexable. Intégration validée en live contre l'API Mistral OCR (texte correctement extrait).
Chaque conversation repartait de zéro alors que les faits du dossier (parties, échéances, conventions de rédaction, préférences) sont une recall à forte valeur. - table project_memories (matter-scoped, jamais globale → pas de contamination inter-clients), chaque fait avec provenance (sourceMessageId) et statut pending/approved ; - extraction best-effort dans onFinish (gated par LOUIS_MEMORY_EXTRACTION, off par défaut — coût LLM), crée des faits « pending » ; - recall : SEULS les faits VALIDÉS par un humain sont injectés, comme contexte NON-FIABLE (réutilise le canal du durcissement prompt-injection) — un fait jamais validé n'influence aucune réponse ; - écran /settings/memory : valider / remettre en attente / supprimer. Vérifié de bout en bout (DB live + Playwright authentifié) : recall n'expose que l'approved ; la page rend correctement ; le clic « Valider » bascule pending→approved. Migration 0009 + var documentée.
L'auth était mono-facteur (mot de passe bcrypt). Un admin détient les clés des données clients et le rayon de souffle du chiffrement at-rest — le mono-facteur est le maillon faible d'un déploiement auto-hébergé. - lib/totp.ts : TOTP maison (HMAC-SHA1, base32, fenêtre ±1, otpauth URI, codes de secours) — zéro dépendance (testé : 7 cas) ; - colonnes users : totp_secret / totp_secret_pending / totp_enabled / backup_codes (codes de secours HACHÉS bcrypt, à usage unique) ; - login : champ « Code 2FA » ; authorize exige un code TOTP valide OU un code de secours quand la 2FA est active (audit auth.totp.failed) ; - écran /settings/security : enrôlement par clé manuelle (secret + URI otpauth), confirmation par code, affichage unique des codes de secours, désactivation. Vérifié end-to-end (Playwright + DB live) : login refusé sans/avec code erroné, accepté avec code valide ; enrôlement complet (secret → confirmation → activation). Migration 0010. La revalidation de session (commit déprovisionnement) propage déjà le second facteur sans réauth forcée.
…lucination) Un agent pouvait affirmer « j'ai créé la mise en demeure » alors que generate_document avait silencieusement échoué (ok:false) — le livrable n'existait pas mais rien ne le signalait. lib/orchestrator/verify.ts évalue, à partir des parts du tour, le résultat RÉEL des outils effectifs (generate_document / edit_document) via leur enveloppe ToolResult. Déterministe (pas d'appel LLM, donc plus fiable qu'un vérificateur probabiliste sur ce mode d'échec). route.ts trace dans l'audit deliverable.verified / deliverable.failed (avec la raison) — signal de défendabilité pour un livrable juridique. Tests : 5 cas.
…nt multi-tours Les pipelines étaient des DAG fixes : un agent de recherche tournait une fois et ne réfléchissait jamais aux lacunes de sa propre note. Nouveau mode `iterative` : le 1er agent (chercheur) reprend SES PROPRES notes à chaque tour pour creuser les lacunes qu'il a identifiées (consignes round-aware : tour 1 = cadrage + lacunes ; tours suivants = creuser le nouveau), puis le dernier agent produit une note de recherche synthétique. Reste SOUVERAIN — les sources sont celles des outils (Légifrance/Pappers/documents), jamais le web. Borné à 4 tours. Câblé partout : types (schema + orchestrator), estimateCalls (rounds + synthèse), validation zod, mode-meta + sélecteur board + bouton tours. Logique testée avec agent factory mocké (chercheur ×rounds, synthèse ×1, round-awareness, borne). 173 tests.
Chaque appel d'outil était rendu en pill indépendante au fil du message. On regroupe désormais toutes les actions d'un tour dans UNE timeline repliable inspirée des vues d'activité d'agent : - en-tête récapitulatif « N outils · X documents · Y recherches » + durée (somme des latences d'agents) + repli global ; - une ligne par outil : icône typée, libellé, résumé d'entrée, chip de catégorie (Légifrance, recherche, document, lecture, MCP) ; - ligne dépliable → détail : carte riche existante (download de document, citations Légifrance/Pappers, sources documentaires) réutilisée pour les outils à rendu dédié, sinon bloc JSON (entrée+sortie) avec bouton de copie ; - les livrables (generate/edit_document) sont dépliés par défaut ; - terminateur « Terminé ». Nouveau lib UI : tool-timeline.tsx (timeline + JsonDetail), tool-meta.ts (icône/chip/catégorie par outil). chat-shell consolide les parts tool-* en une timeline rendue à la position du premier outil. Vérifié via Playwright (rendu pleine largeur conforme à la maquette, expansion JSON, doc auto-déplié).
Le cadre bordé et le fond rendaient le bloc trop lourd. On enlève le conteneur (la timeline flotte directement dans le chat), on allège : cercles d'icônes en fin trait sur fond transparent, chips discrets, plus d'air entre les lignes, plus de fond au survol. Tout est désormais REPLIÉ par défaut (le détail — carte de document ou JSON — s'ouvre au clic), ce qui supprime aussi la pill redondante qui apparaissait sur les livrables en cours de génération. Vérifié via Playwright (collapsed + expansion).
Le menu d'actions du composer n'exposait que Providers/Modèles côté réglages. On ajoute les raccourcis Skills (/settings/skills), Connecteurs (/settings/connectors) et Serveurs MCP (/settings/mcp), avec les mêmes libellés et icônes que la navigation des réglages.
…G Louis) Bug : « Joindre un document » dans le menu « + » ouvrait un Popover ancré à un trigger CACHÉ, ouvert programmatiquement depuis le DropdownMenu → l'événement de fermeture du dropdown était capté par onPointerDownOutside du Popover, qui se refermait aussitôt. Correctif + UX : on sort le trombone du menu « + » et on le place dans la barre d'input comme VRAI déclencheur du Popover (plus de fermeture immédiate). Son menu propose « Téléverser depuis l'ordinateur » (input fichier → handleDroppedFiles) ET la liste des documents existants de Louis (RAG) à cocher. Le menu « + » ne porte plus « Joindre un document ». Vérifié via Playwright (popover stable, upload + liste RAG présents).
…us-dossiers) Le picker du trombone listait les documents à plat. Il reflète désormais la vraie arborescence : dossiers et sous-dossiers (parentFolderId) repliables, documents en feuilles avec case à cocher, indentés selon la profondeur. - page.tsx remonte folderId par document + la liste des dossiers de l'utilisateur ; - DocPickerContent construit l'arbre (childrenByParent / docsByFolder), élague les dossiers sans aucun document (direct ou descendant), et place les documents sans dossier (ex. fichiers tout juste téléversés) à la racine. Vérifié via Playwright (sous-dossier « Pièces » imbriqué sous « Dossier ACME », doc racine présent, dossier vide élagué).
L'état vide donnait des infos datées (« icône étoiles » / « Insérer un workflow » alors que ce sont les Trames du menu « + »). On le remplace par : - 4 exemples cliquables (mise en demeure, jurisprudence Légifrance, résumé de décision, responsabilité civile) qui pré-remplissent le composer et le focus ; - un pied de page court et EXACT : trombone (pièce ou document de Louis), « + » (trames, board multi-agents, réglages), badge FR/UE/US de souveraineté ; - une aide (i) reformulée (Légifrance/Pappers, rédaction .docx, outils inspectables). Vérifié via Playwright (mentions datées absentes, clic sur une suggestion remplit le composer).
computeCost faisait un match EXACT sur la table de tarifs : un modèle hors table
(ex. gpt-5.5) renvoyait null → « — » au lieu d'un coût (page Coûts & usage : mois
+ détail par modèle).
resolveModelPricing devient tolérant : match exact → ID normalisé (suffixe de
date retiré) → famille par PRÉFIXE (gpt-5, gpt-4.x, gpt-4o, claude-{opus,sonnet,
haiku}-4, mistral-{large,medium,small}), du plus spécifique au plus général. Les
IDs versionnés/datés (gpt-4o-2024-08-06, claude-opus-4-8) et les nouveaux modèles
d'une famille connue (gpt-5.5) sont désormais tarifés. Ajout des familles gpt-5
(estimations à réviser, couvertes par le disclaimer de la page). Un modèle
vraiment inconnu (auto-hébergé) reste à null → « — » (comportement voulu).
Tests : 7 cas.
Le layout des réglages ne pose aucun padding sur la zone de contenu — chaque page doit fournir son propre conteneur. Les deux pages que j'avais ajoutées (Sécurité, Mémoire) n'avaient qu'un <div max-w-2xl> sans padding ni centrage → contenu collé en haut à gauche, contrairement aux autres pages. On adopte le conteneur standard (mx-auto w-full max-w-3xl px-6 py-8 md:px-8 md:py-10) + l'en-tête à eyebrow (catégorie en capitales + titre font-heading + description), comme Général/Skills/Coûts. Corrige aussi un espace avalé par JSX (« validépar vous » → « validé par vous »). Vérifié via Playwright.
L'enrôlement 2FA n'offrait que la saisie manuelle de la clé. On affiche désormais un QR code de l'URI otpauth:// (lib qrcode.react — rendu SVG 100 % local, aucune requête externe → compatible souveraineté) à scanner directement avec Google Authenticator / Aegis / 1Password. Fond blanc + quiet zone pour rester scannable en thème sombre. La saisie manuelle de la clé reste disponible en repli (« Impossible de scanner ? »). Vérifié via Playwright.
…file L'ajout de qrcode.react avait été fait via `npm install` sur macOS, qui a élagué du package-lock.json les deps optionnelles spécifiques à d'autres plateformes (@emnapi/core@1.10.0, @emnapi/runtime@1.10.0). Résultat : `npm ci` réussit sur macOS mais ÉCHOUE sur la CI Linux (« Missing @emnapi/... from lock file »), cassant les 4 jobs qui installent les deps. On repart du lockfile complet (multi-plateforme) et on y réinjecte uniquement qrcode.react, sans rien élaguer. Validé par `npm ci` (cohérent) + tsc + 180 tests.
…aire Sécurité & authentification - 2FA : la désactivation exige un code TOTP courant (step-up) ; rate-limit par compte sur la vérification TOTP / codes de secours - Anti-énumération des comptes : comparaison bcrypt à temps constant pour les comptes inconnus ou inactifs - Invalidation des sessions JWT au changement de mot de passe et à la désactivation 2FA (colonne users.token_version, comparée à chaque requête) - requireUserId centralisé dans lib/auth/permissions Fiabilité du chat - Régénération/édition : plus de doublons (message utilisateur dupliqué, réponses empilées) — la route lit le trigger et REMPLACE la réponse précédente (message + trail d'audit) au lieu de l'empiler - Erreurs de stream loguées (onError) et persistance gardée : un échec DB ne fait plus disparaître une réponse déjà streamée sans trace - Transactions sur les séquences multi-écritures (édition+élagage, défaut provider, création projet, clonage pipeline) Génération documentaire - Round-trip éditeur → DOCX : échappement des _ / * littéraux (fin de la corruption du texte d'un acte : placeholders, blancs à remplir) - read_document reflète les insertions/suppressions suivies - Listes multi-niveaux préservées à l'export (numérotation 1. → a. → i.) - Mode itératif de l'orchestrateur réparé (il était silencieusement exécuté en séquentiel) Données & audit - Index sur les clés étrangères chaudes (messages.conversation_id, etc.) - Journal d'audit complété : serveurs MCP, changement de mot de passe, sauvegarde de document - Estimation de coût alignée sur les plafonds réels d'exécution - Validation du SIREN avant appel Pappers ; type ActionResult partagé Interface - Refonte de l'écran de connexion ; finitions et animations du chat Tests : 210 passants.
Le lockfile référençait @emnapi/runtime et @emnapi/core (deps WASM/napi optionnelles, tirées par les nouvelles deps Tiptap de l'éditeur) sans les définir → `npm ci` échouait en CI (EUSAGE, lock désynchronisé). Lockfile régénéré (npm install --package-lock-only) : graphe complet et cohérent, deps Tiptap conservées. Validé localement par `npm ci`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ce que cette branche apporte
Chat — expérience
Chat — fiabilité
onError) + persistance gardée : un échec DB ne fait plus disparaître une réponse déjà streamée sans traceSécurité & authentification
users.token_version)requireUserIdcentraliséGénération documentaire
_/*littéraux (fin de la corruption du texte d'un acte)read_documentreflète les insertions/suppressions suivies ; listes multi-niveaux préservées à l'exportDonnées & audit
messages.conversation_id, etc.)ActionResultpartagéSchéma DB
Changements additifs uniquement (colonne
users.token_version, nouveaux index). À appliquer vianpm run db:push.Tests : 210 passants ·
tsc0 · build OK.