Release 2026-06-02 — Audit liens morts backoffice + rate limit LLM + Aïssa support persona
Status : en production. tauraco.io app SPA déployé, backend redéployé, widget support déployé sur LSWS et bust-cache CF appliqué.
TL;DR
Trois jets sur la même journée :
- Audit dead-links backoffice (
/app/*) sur 4 profils — 332 nav tests, 6 bugs corrigés, 0 lien mort en fin de journée. - Prompt caching + payload sans état sur les 4 endpoints LLM — coût input divisé par ~5 sur les sessions multi-tours.
- Rate limit quotidien Redis sur tous les endpoints LLM (cap même en premium) + redesign complet du widget support tauraco.io en persona « Aïssa » humaine, animée, avec réponses courtes structurées.
| Avant | Après |
|---|---|
| 3 pages SUPER_ADMIN crashent en console (51 erreurs cumulées) | 0 erreur, 0 crash |
| 3 endpoints API morts laissaient des pages avec des consoles errors | Catch 404 propres, empty state propre |
| 1 form après création FX rate restait bloqué | Redirige correctement vers la liste |
Note rétroactive
La section initiale de cette release notes indiquait « aucun changement fonctionnel sur les assistants aujourd'hui ». Cela a été corrigé en fin de journée par les PR #50, #52, #53 — voir les sections en bas du document. À noter : la persona Aïssa est ce que voient les visiteurs non connectés sur tauraco.io ; le requêteur authentifié (/app/assistant) n'a pas changé d'apparence (uniquement caching + rate limit côté serveur).
Méthode de l'audit
Crawl Playwright qui pour chaque profil :
- Set le JWT en localStorage (
tauraco:refresh) - Navigue à chaque route nommée du menu via
router.push({ name }) - Capture l'URL résolue, le texte rendu, console.error, fetch failures
- Classe : granted / forbidden / NOT_FOUND / STUCK_LOADING / ERROR_STATE
Profils utilisés :
| Profil | User | Tenant | Rôle |
|---|---|---|---|
| INVESTOR | [email protected] | t_platform | INVESTOR |
| SELLER | [email protected] | t_demo_001 | SELLER |
| ERP_OWNER | [email protected] | t_erp_demo_001 | ERP_OWNER |
| SUPER_ADMIN | [email protected] | t_demo_001 | SUPER_ADMIN |
Bugs trouvés et corrigés
PR #47 — Premier crawl : 3 bugs critiques sur les pages SUPER_ADMIN
| # | URL | Symptôme | Cause | Fix |
|---|---|---|---|---|
| 1 | /app/erp/fx-rates/new après Create | Bloqué sur le form, pas de retour à la liste | router.push({name:'erp-fx-rates'}) — la vraie route est erp-fx ; le .catch(()=>{}) du dispatcher swallow l'erreur | name: 'erp-fx' |
| 2 | /app/compliance/fees | Crash Cannot read properties of undefined (reading 'length') | adminPortalService appelait /v3/admin/fees (endpoint summary depuis renommage) ; CRUD vit à /v3/admin/payments/fees | switched + res?.items ?? [] |
| 3 | /app/ops/audit | 50× t?.some is not a function PrimeVue | expandedRows typé {} mais PrimeVue v4 attend un array | unknown[] = [] |
PR #48 — Re-crawl post-fix : 3 endpoints API morts
Le re-crawl a fait remonter des console.error "Failed to load resource 404" même si les pages rendaient leur empty state correctement. Trois endpoints obsolètes :
| # | Endpoint frontend | État backend | Fix |
|---|---|---|---|
| 4 | /v1/approvals/instances (pipelineService) | n'existe pas | renommé en /v1/approvals/requests (le vrai path) |
| 5 | /v2/admin/contact-requests (contactService) | non implémenté | .catch(404) → { items: [], total: 0 } |
| 6 | /v2/admin/support/conversations* (supportChatService) | non implémenté | helper _safeEmpty<T>() qui retourne [] sur 404 |
Les deux endpoints admin (#5, #6) restent à implémenter côté backend si l'on veut un jour lister les contact-requests et l'historique des conversations support. Pas d'urgence : les empty states s'affichent correctement en attendant.
Audit final post-déploiement
| Profil | Routes accordées | Forbidden (attendu) | Anomalies UI | console.error | HTTP failures |
|---|---|---|---|---|---|
| INVESTOR | 12 | 71 | 0 | 0 | 0 |
| SELLER | 8 | 75 | 0 | 0 | 0 |
| ERP_OWNER | 38 | 45 | 0 | 0 | 0 |
| SUPER_ADMIN | 83 | 0 | 0 | 0 | 2 swallowed |
Total : 332 nav tests, 0 lien mort, 0 erreur Vue/Pinia/PrimeVue.
---
PR #50 — Prompt caching Anthropic + payload stateless
Problème : chaque tour du widget support et du requêteur renvoyait tout l'historique de conversation à Anthropic, ce qui faisait grossir le coût input linéairement avec la session.
Fix :
cache_control: {type: "ephemeral"}sur les system prompts des 4 endpoints LLM (/v1/support/chat,/v2/ai/chat,/v2/ai/insights,/v2/ai/requeteur). Le prompt ~3 KB est envoyé une fois par fenêtre de 5 min puis servi depuis le cache Anthropic → ~80 % d'économie input sur les sessions multi-tours.- Côté client (widget statique +
RequeteurChatView.vue), envoi du seul dernier message utilisateur dansmessages. Le serveur est désormais stateless par tour, le cache Anthropic remplace l'historique pour la cohérence du système. - Tools array du requêteur également marqué
cache_controlephemeral.
PR #52 — Daily rate limit Redis sur tous les endpoints LLM + persona Aïssa
Problème : aucun garde-fou empêchait un script ou une fuite de clé de drainer le budget Anthropic via les endpoints LLM. Le requêteur avait paywall premium mais pas de cap par jour. Les 3 autres endpoints n'avaient rien.
Fix backend — app/services/llm_rate_limit.py (nouveau, Redis-backed) :
| Endpoint | Cap quotidien | Identité |
|---|---|---|
/v1/support/chat | 30 | par IP (header CF-Connecting-IP aware) |
/v2/ai/chat | 100 | par user |
/v2/ai/insights | 100 | par user |
/v2/ai/requeteur | 200 | par user (premium) |
- Caps configurables via
settings.LLM_RATE_LIMIT_*— pas besoin de redéploiement pour les ajuster. - Cap appliqué même en premium : explicitement demandé par sega pour borner la dépense Anthropic si une clé fuite.
- Compteur
INCR + EXPIREsur cléllm:rl:<endpoint>:{user|ip}:<YYYY-MM-DD>qui survit 26 h pour absorber le rollover UTC. - HTTP 429 avec headers
Retry-After,X-RateLimit-Limit,X-RateLimit-Used. - Fail-open : si Redis tombe, on ne bloque pas les vrais users (logué, mais pas de 503 ni de 429).
Fix support_chat.py — persona « Aïssa » :
- System prompt entièrement réécrit : nom Aïssa, ton conseillère, ≤ 60 mots, structure imposée « 1 phrase d'accroche → 2-4 puces → 1 CTA », tutoie/vouvoie selon le visiteur, FR/EN selon la langue détectée, JAMAIS inventer une feature absente de la liste produit.
max_tokensbaissé de 2048 → 400.
Fix frontend — frontend/js/support-chat.js (redesign complet, ~14 pages publiques chargent le widget) :
- FAB « Aï » initiales en gradient vert (plus de 💬), ring « breathing », point en ligne toujours visible.
- Window slide-in avec overshoot doux ; chaque bulle de message fade-in.
- Greeting + 3 quick replies (Voir les offres / Je ne peux pas me connecter / Demander une démo) après 700 ms de typing indicator au premier ouvrage.
- Markdown-lite étendu : puces
-, auto-lien[email protected]. - Nudge bubble « Une question ? » 6 s après chargement, scoped session.
- HTTP 429 surfacé proprement : message poli + input verrouillé, pas de retry silencieux.
- Stateless : un seul message dans le payload.
PR #53 — Bump cache-buster v=14.0.0 → v=14.0.1
Problème : Cloudflare avait mis en cache l'URL ?v=14.0.0 avec le body pre-deploy (le docroot LSWS n'avait pas encore été synchronisé au premier hit). CF servait ensuite le body obsolète comme immutable pendant 7 jours → aucun visiteur ne voyait Aïssa.
Fix : bump du buster pour contourner l'entrée CF morte. Pas de token CF purge nécessaire.
Smoke test prod (Playwright sur tauraco.io)
| Vérif | Résultat |
|---|---|
| FAB rendu « Aï » 64 px (EN + FR) | ✅ |
| Window s'ouvre avec typing + greeting | ✅ |
| Quick replies cliquables, round-trip LLM | ✅ |
Réponse EN — What are your plans? → 1 hook + 3 bullets + CTA, ~74 mots | ✅ |
Réponse FR — Je n'arrive pas à me connecter → 3 étapes (reset / email invit / sega) en tutoiement | ✅ |
Payload réseau : {messages:[{role:"user",content:"…"}]} seul (stateless) | ✅ |
| 429 forcé EN → « Daily request limit reached… » + input lock | ✅ |
| 429 forcé FR → « Trop de requêtes pour aujourd'hui… » + input lock | ✅ |
PRs du jour
| PR | Titre | Insertions | Deletions |
|---|---|---|---|
| #47 | fix(backoffice): repair 3 dead/broken pages | 12 | 8 |
| #48 | fix(backoffice): graceful 404 on 3 dead API endpoints | 31 | 7 |
| #49 | docs(release): 2026-06-02 backoffice dead-link audit notes | — | — |
| #50 | perf(ai): Anthropic prompt caching + stateless per-turn payload | 624 | 36 |
| #52 | feat(llm + widget): daily rate limit + Aïssa support persona | 516 | 195 |
| #53 | chore(widget): bump cache buster to v=14.0.1 (CF stale fix) | 14 | 14 |
| Total | ~1 200 | ~260 |
Suite (deferred — pas urgents)
- Backend : implémenter
/v2/admin/contact-requests(listing) et/v2/admin/support/conversations*si on veut surfacer ces données dans l'admin. Pour l'instant les pages restent fonctionnelles via empty state. - Frontend : retirer le silent-catch
router.push().catch(() => {})dansAppSidebar.vue:80— il a masqué le bugerp-fx-ratespendant longtemps. Logger les nav failures en console pour faciliter le debug futur. - Observabilité rate limit LLM : surfacer
X-RateLimit-Used / Limitdans l'UI requêteur pour que l'utilisateur premium voie sa consommation quotidienne avant de toucher le 429. - Tuning caps : reconsidérer les valeurs après quelques jours d'observation (Redis SCAN
llm:rl:*pour stats). 30/IP sur/v1/support/chatpeut être bas si plusieurs collègues d'une même entreprise testent depuis le même WAN.