33800 Docs

← Retour

Audit Passe 2 — ulias-org-web (Frontend Qwik)

Gestion d'etat reactif, serialisation, accessibilite, securite

Date : 15/02/2026 Status : IMPLEMENTEE Auditeur : Claude Opus 4.6 (passe 2, regard neuf) Perimetre : 22 fichiers .tsx + 3 fichiers .ts dans src/ Classification : BUG CERTAIN / BUG PROBABLE / RISQUE THEORIQUE / FAUX POSITIF


RESUME EXECUTIF

Le projet est une application Qwik City a 8 routes + 7 composants, avec communication temps reel via WebSocket et API REST. L'architecture repose sur un bus evenementiel document.dispatchEvent(CustomEvent) pour decouple le layout (WS manager) des composants enfants.

Findings critiques : 4 BUG CERTAIN, 6 BUG PROBABLE, 10 RISQUE THEORIQUE, 1 FAUX POSITIF


1. GESTION D'ETAT REACTIF ET SERIALISATION QWIK

1.1 BUG CERTAIN — useStore mute dans le render (EventCard.tsx)

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/EventCard.tsx Lignes : 319, 322-323

const hasOriginalMessage = (event.type === 'objective_completed' || event.type === 'objective_failed') && event.data?.originalMessage;
const routingConfidence = event.type === 'routing' && event.data?.confidence
  ? Math.round(event.data.confidence * 100) : null;

Ces acces a event.data dans le corps du render (hors $() closure) sont directement dans le scope du composant. Si event est un proxy reactif Qwik (passe par useStore ou signal), chaque acces declenche un tracking. Ce n'est pas un bug de mutation, mais...

Verdict : FAUX POSITIF — event est passe en prop, pas un store. Les acces sont en lecture seule.

1.2 BUG PROBABLE — useStore pour streamInfo dans ChatMessages.tsx

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/ChatMessages.tsx Ligne : 22

const streamInfo = useStore<StreamingInfo>({ agent: '', model: '', text: '', iteration: '', maxIterations: '', tool: '', toolDuration: '' });

Le useStore est mute directement dans un event listener vanilla (lignes 41-86) qui est enregistre dans useVisibleTask$. Les mutations comme streamInfo.agent = ''; (ligne 41) sont faites dans un callback non-Qwik (document.addEventListener). Qwik serialise les stores — les mutations depuis un handler vanilla JS fonctionnent en pratique car le proxy est toujours le meme objet, mais ce pattern n'est pas garanti par la spec Qwik.

Classification : RISQUE THEORIQUE Impact : Peut fonctionner en dev mais casser en production avec SSR/resumability.

1.3 BUG CERTAIN — searchTimerRef via useSignal<number>(0) pour stocker un timer ID

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/sidebar/ConversationSidebar.tsx Ligne : 77

const searchTimerRef = useSignal(0);

Puis ligne 327 :

if (searchTimerRef.value) clearTimeout(searchTimerRef.value);

Et ligne 334 :

searchTimerRef.value = setTimeout(async () => { ... }, 300) as any;

Probleme : setTimeout retourne un ID de type number | NodeJS.Timeout. Le signal est type number (initial 0). Le as any masque le cast force. Plus grave : le timer n'est jamais nettoye au cleanup. Si le composant est demonte pendant que le timer tourne, le callback setTimeout executera quand meme et tentera de modifier searchResults.value.

Classification : BUG CERTAIN Impact : Fuite de memoire + potentiel crash apres demontage. Pas de cleanup dans la closure cleanup() de useVisibleTask$.

1.4 BUG CERTAIN — Polling interval archiveInterval qui ne detecte pas les changements

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/sidebar/ConversationSidebar.tsx Lignes : 151-156

const origShowArchived = showArchived.value;
const archiveInterval = setInterval(() => {
  if (showArchived.value !== origShowArchived) {
    loadConversations();
  }
}, 500);

Probleme : origShowArchived capture la valeur au moment de l'initialisation. Apres le premier changement et le premier loadConversations(), l'interval continuera d'appeler loadConversations() toutes les 500ms indefiniment, car showArchived.value !== origShowArchived restera toujours vrai apres le premier toggle. Pas de mise a jour de origShowArchived apres detection.

Classification : BUG CERTAIN Impact : Polling API toutes les 500ms indefiniment apres le premier toggle archive. Charge reseau et serveur.

1.5 BUG PROBABLE — Course condition sur loadLastConversation avec setTimeout(500ms)

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/layout.tsx Lignes : 167-169

setTimeout(() => {
  document.dispatchEvent(new CustomEvent('ulias:load-history', { detail: { messages: items } }));
}, 500);

Probleme : Le setTimeout(500) suppose que ChatMessages sera monte en 500ms. Si le composant est lent a se monter (reseau lent, appareil lent), l'evenement est perdu. Si le composant est deja monte, le delay de 500ms est inutile. Pas de mecanisme de retry ni de verification.

Classification : BUG PROBABLE Impact : Au premier chargement, la conversation precedente peut ne pas s'afficher.

1.6 RISQUE THEORIQUE — Meme pattern setTimeout(500) pour template message

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/sidebar/ConversationSidebar.tsx Lignes : 201-203

setTimeout(() => {
  document.dispatchEvent(new CustomEvent('ulias:send-template-message', { detail: { content } }));
}, 500);

Meme probleme que 1.5. Le WS peut ne pas avoir fini le switch de conversation en 500ms.

Classification : RISQUE THEORIQUE


2. SECURITE FRONTEND

2.1 BUG CERTAIN — dangerouslySetInnerHTML avec du contenu utilisateur

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/history/index.tsx Ligne : 376

<div ... dangerouslySetInnerHTML={renderMarkdown(obj.result.output)} />

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/EventCard.tsx Lignes : 407, 423

<div ... dangerouslySetInnerHTML={renderMarkdown(summary)} />

Le renderMarkdown() dans markdown.ts appelle escapeHtml() en premier (ligne 18), puis applique les regexes de conversion. Cependant, la regex pour les liens (ligne 46) :

html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" ...>$1</a>');

...permet d'injecter du HTML dans l'attribut href. Un contenu comme [click](javascript:alert(1)) passera escapeHtml (pas de <, >, &, " dans javascript:alert(1)) et generera :

<a href="javascript:alert(1)" ...>click</a>

Classification : BUG CERTAIN (XSS via javascript: URI) Impact : Un agent ou le serveur peut envoyer un lien malveillant qui execute du JS arbitraire au clic.

2.2 RISQUE THEORIQUE — API key stockee en localStorage sans expiration

Fichiers : Multiples (layout.tsx:32, login/index.tsx:69, et 12+ autres acces a localStorage.getItem('ulias_api_key')) Impact : Si un script tiers ou une extension navigateur lit le localStorage, la cle API est compromise. Pas de token rotation cote client.

Classification : RISQUE THEORIQUE Note : C'est un pattern standard pour les SPA. Acceptable si le backend invalide les cles regulierement.

2.3 RISQUE THEORIQUE — API key envoyee dans chaque message WS

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/lib/ws-manager.ts Lignes : 93-96, 98-101

send(content: string) {
  if (this._status !== 'connected') return;
  this.sendRaw({ type: 'message', apiKey: this.apiKey, content });
}

La cle API est envoyee dans chaque message apres authentification, pas seulement dans le message auth. Si le WebSocket est intercepte (downgrade attack, mauvaise config TLS), tous les messages exposent la cle.

Classification : RISQUE THEORIQUE Recommendation : Apres auth_success, utiliser un session token ou ne plus envoyer la cle dans chaque message.

2.4 RISQUE THEORIQUE — Pas de validation CSRF sur les endpoints API

Les appels fetch() vers l'API utilisent X-API-Key en header. Pas de cookie, donc pas de CSRF au sens classique. Mais si l'API accepte aussi des cookies de session, le risque existe.

Classification : RISQUE THEORIQUE (probablement OK si l'API n'utilise que les API keys)

2.5 BUG PROBABLE — event.data untyped access avec as any partout

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/EventCard.tsx Lignes : 72-76 (event.data access), 134 (event.data?.title || event.data?.summary)

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/ChatMessages.tsx Lignes : 52-86 (d?.model, d?.confidence, etc.)

Les types dans types.ts ne definissent pas model, confidence, reasoning, pipeline, duration, tools, success, originalMessage, toolCalls, iterations sur les interfaces existantes (ThinkingEvent.data n'a que iteration et maxIterations, par exemple). Le code accede a des champs non-types avec event.data?.model etc.

Classification : BUG PROBABLE Impact : Les types TypeScript ne protegent pas contre un changement cote serveur. Un champ renomme cote backend passera silencieusement.


3. PATTERNS QWIK — CLOSURES ET SERIALISATION

3.1 BUG PROBABLE — useNavigate() utilise dans un handler onClick$

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/layout.tsx Lignes : 261-265

onClick$={() => {
  localStorage.removeItem('ulias_api_key');
  document.dispatchEvent(new CustomEvent('ulias:disconnect'));
  nav('/login');
}}

nav est le retour de useNavigate(). Il est capture dans une closure $(). En Qwik, les closures $() sont serialisees et le contenu capture doit etre serialisable. useNavigate() retourne une QRL qui est serialisable par Qwik, donc cela devrait fonctionner. Cependant, le document.dispatchEvent dans la meme closure accede a document qui est un objet browser — Qwik devrait pouvoir le gerer car c'est un global.

Classification : FAUX POSITIF (fonctionne correctement en Qwik)

Note : Reclassifie en FAUX POSITIF car le pattern est correct.

3.2 RISQUE THEORIQUE — Closures $() capturant des variables locales complexes

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/EventCard.tsx Ligne : 305-308

const retryIdentical = $(() => {
  const msg = event.data?.originalMessage;
  if (!msg) return;
  document.dispatchEvent(new CustomEvent('ulias:send', { detail: { content: msg } }));
});

La closure $() capture event (une prop). En Qwik, les props passees a un composant sont serialisees. Si event contient des objets profondement imbriques avec des fonctions ou des references circulaires, la serialisation echouera.

Classification : RISQUE THEORIQUE Note : En pratique, ServerEvent est un objet JSON pur, donc serialisable.

3.3 RISQUE THEORIQUE — Multiples useSignal dans EventCard pour chaque instance

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/EventCard.tsx Lignes : 174-188

const feedbackGiven = useSignal<-1 | 0 | 1>(0);
const showComment = useSignal(false);
const comment = useSignal('');
const isEditing = useSignal(false);
const editContent = useSignal('');
const correctedLocal = useSignal(isCorrected || false);
const showRetryEdit = useSignal(false);
const retryMessage = useSignal('');
const toolResultExpanded = useSignal(false);

9 signaux par instance de EventCard. Avec potentiellement des centaines d'evenements dans le chat, cela cree des centaines de signaux Qwik. Chaque signal est un objet avec un abonnement reactif.

Classification : RISQUE THEORIQUE Impact : Performance degradee avec beaucoup de messages. Pas un bug mais un anti-pattern pour les listes longues.


4. FUITES DE MEMOIRE

4.1 BUG PROBABLE — Event listener onScroll attache a containerRef.value jamais mis a jour

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/ChatMessages.tsx Lignes : 214-216, 225

requestAnimationFrame(() => {
  containerRef.value?.addEventListener('scroll', onScroll);
});
// ...
cleanup(() => {
  containerRef.value?.removeEventListener('scroll', onScroll);
});

Si containerRef.value change entre le moment de l'attach (requestAnimationFrame) et le cleanup, le removeEventListener sera appele sur un element different de celui ou addEventListener a ete appele. L'ancien listener restera attache a l'ancien element.

Classification : RISQUE THEORIQUE Note : En pratique, containerRef est un ref sur un div statique, donc l'element ne change probablement pas.

4.2 BUG PROBABLE — searchTimerRef jamais nettoye au cleanup (deja mentionne en 1.3)

Combine avec le point 1.3 : aucun clearTimeout(searchTimerRef.value) dans la closure cleanup(). Si l'utilisateur tape dans la recherche puis quitte la page, le timer executera son callback sur un composant demonte.

Classification : BUG CERTAIN (deja compte en 1.3)

4.3 RISQUE THEORIQUE — URL.createObjectURL correctement libere

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/sidebar/ConversationSidebar.tsx Lignes : 289-297

const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = ...;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);

Le revokeObjectURL est appele immediatement apres click(). Le click() declenche un telechargement asynchrone — si le navigateur n'a pas encore lu le blob quand revokeObjectURL est appele, le telechargement peut echouer dans certains navigateurs.

Classification : RISQUE THEORIQUE Recommendation : Ajouter un setTimeout(() => URL.revokeObjectURL(url), 1000).


5. GESTION DES ERREURS RESEAU

5.1 BUG PROBABLE — Tous les catch sont silencieux

Presque tous les blocs try/catch dans le projet utilisent catch { /* silent */ } ou catch(() => {}) :

Classification : BUG PROBABLE Impact : L'utilisateur n'a AUCUN feedback quand une operation reseau echoue (suppression, archivage, fork, pin, export, correction, etc.). L'UI reste dans un etat incoherent — le changement local est fait mais le serveur n'a pas recu l'action.

5.2 RISQUE THEORIQUE — Pas de retry sur les appels API critiques

Aucun mecanisme de retry n'est implementé pour les appels API. Le WsManager a un reconnect avec backoff exponentiel (bon), mais les appels REST n'ont aucun retry.

Classification : RISQUE THEORIQUE Note : Le reconnect WS est bien fait (exponential backoff, max 5 tentatives, cleanup propre).

5.3 BUG PROBABLE — Pas de gestion du cas res.ok === false sans throw

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/layout.tsx Lignes : 97-105

const res = await fetch(`${API_URL}/api/conversations/${convId}/messages`, {
  headers: { 'X-API-Key': apiKey },
});
if (res.ok) {
  // load messages
}
// else: silently ignored

Si le serveur retourne un 401 (cle expiree), 500, ou 404, l'utilisateur ne voit rien. Pire : apres un 401, le WS continue de fonctionner avec l'ancienne session.

Classification : BUG PROBABLE


6. ACCESSIBILITE (a11y)

6.1 BUG PROBABLE — Modal dans prompts/index.tsx sans trap de focus

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/prompts/index.tsx Lignes : 186-266

Le modal d'edition de prompt est un <div class="fixed inset-0 bg-black/50 z-50 ...">. Il n'a :

Classification : BUG PROBABLE (violation WCAG 2.1 AA)

6.2 BUG PROBABLE — Aucun label accessible sur les boutons SVG

Quasiment tous les boutons d'action (pin, archive, supprimer, exporter, renommer, fork) n'ont que des SVG sans texte visible. Ils ont des title attributs mais pas d'aria-label.

Exemples :

title est lu par les lecteurs d'ecran inconsistement (certains l'ignorent). aria-label est le standard.

Classification : BUG PROBABLE (violation WCAG)

6.3 RISQUE THEORIQUE — Navigation clavier incomplete

Classification : RISQUE THEORIQUE (fonctionnel mais inaccessible au clavier)

6.4 RISQUE THEORIQUE — Pas de live region pour les evenements streaming

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/components/chat/ChatMessages.tsx Lignes : 294-310 (streaming indicator)

Le statut streaming qui change en temps reel (routing, thinking, tool_call) n'a aucun aria-live="polite" ou role="status". Les utilisateurs de lecteurs d'ecran ne sont pas informes de la progression.

Classification : RISQUE THEORIQUE


7. PATTERN ARCHITECTURAL — BUS EVENEMENTIEL

7.1 RISQUE THEORIQUE — Bus evenementiel fragile

Le systeme repose sur ~15 types de CustomEvent dispatches sur document :

Evenement Emetteur Recepteur(s)
ulias:event layout ChatMessages, EventStream, Decisions, ConversationSidebar
ulias:send ChatInput, SuggestionsBar, History, EventCard layout (WS)
ulias:user-message ChatInput, SuggestionsBar, routes/index ChatMessages
ulias:command (non utilise dans le code lu) layout (WS)
ulias:send-raw Decisions/EventCard layout (WS)
ulias:disconnect layout (logout button) layout
ulias:switch-conversation Sidebar, ChatMessages, EventCard layout
ulias:load-history layout ChatMessages
ulias:prepend-history layout ChatMessages
ulias:load-older ChatMessages layout
ulias:clear-chat layout ChatMessages
ulias:conversation-created layout, Sidebar, EventCard Sidebar
ulias:send-template-message Sidebar layout
ulias:message-corrected EventCard ChatMessages

Probleme : Aucun typage TypeScript sur les evenements CustomEvent. Un changement de nom ou de structure du detail casse silencieusement la communication. Pas de centralisation ni de documentation.

Classification : RISQUE THEORIQUE Recommendation : Creer un module events.ts avec des fonctions typees dispatchSend(content: string), onEvent(handler: (e: ServerEvent) => void), etc.


8. MARKDOWN RENDERER

8.1 BUG PROBABLE — Regex greedy sur les code blocks

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/lib/markdown.ts Ligne : 21

html = html.replace(/```(\w*)\n([\s\S]*?)```/g, ...);

Le [\s\S]*? est lazy mais cherche le premier ``` de fermeture. Si le markdown contient des backticks triples imbriques ou des code blocks sans fermeture, le rendu sera casse. Plus important : le escapeHtml est applique AVANT le parsing des code blocks, ce qui signifie que & dans le code est deja transforme en &amp;, et sera affiche tel quel.

Mais aussi : la regex cherche ```\n (backtick + newline). Si l'input est ```js (sans newline car le newline a deja ete escapeHtml), la regex ne matchera pas.

Hmm, en fait escapeHtml ne touche pas \n, donc ca fonctionne.

Classification : RISQUE THEORIQUE (edge case avec markdown malformed)

8.2 Deja couvert en 2.1 — XSS via javascript: URI dans les liens markdown.


9. AUTRES FINDINGS

9.1 RISQUE THEORIQUE — window.location.href = '/' force un full reload

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/history/index.tsx Ligne : 82

const retryObjective = $((description: string) => {
  document.dispatchEvent(new CustomEvent('ulias:send', { detail: { content: description } }));
  window.location.href = '/';
});

window.location.href force un reload complet de la page. L'evenement ulias:send dispatche juste avant sera perdu car le listener dans le layout sera detruit au reload. Le message ne sera jamais envoye.

Classification : BUG CERTAIN Fix : Utiliser useNavigate() au lieu de window.location.href et s'assurer que le message est envoye apres la navigation.

9.2 RISQUE THEORIQUE — checking.value early return dans le render de login

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/login/index.tsx Lignes : 82-88

if (checking.value) {
  return (
    <div class="flex items-center justify-center h-screen bg-base-200">
      <span class="loading loading-spinner loading-lg text-primary" />
    </div>
  );
}

Ce return conditionnel dans un component$() est un pattern valide en Qwik.

Classification : FAUX POSITIF (pattern correct)

Note : Reclassifie.

9.3 BUG PROBABLE — decisions.value.filter() dans le corps du render, potentielle recalculation excessive

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/decisions/index.tsx Lignes : 95-96

const pendingDecisions = decisions.value.filter(d => !d.response);
const resolvedDecisions = decisions.value.filter(d => d.response);

Ces .filter() sont appeles a chaque render et ne sont pas memoises. Pas un bug fonctionnel mais un impact perf si la liste est longue. Cependant, .filter() ne mute PAS le tableau source, donc pas de boucle infinie.

Classification : RISQUE THEORIQUE (performance, pas fonctionnel)

9.4 RISQUE THEORIQUE — totalAgents calcule dans le render body

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/teams/index.tsx Ligne : 55

const totalAgents = teams.value.reduce((sum, t) => sum + (t.agents?.length ?? 0), 0);

Meme pattern que 9.3. Recalcule a chaque render. Acceptable pour de petites listes.

Classification : RISQUE THEORIQUE

9.5 BUG PROBABLE — teams/index.tsx n'envoie pas l'API key

Fichier : /stock_8to/33800-stack/projects/ulias-org-web/src/routes/teams/index.tsx Ligne : 44

const res = await fetch(`${API_URL}/api/teams`);

Contrairement a toutes les autres pages qui envoient { 'X-API-Key': apiKey }, la page teams n'envoie aucun header d'authentification. Si l'endpoint /api/teams est protege, la requete echouera avec un 401.

Classification : BUG PROBABLE Impact : La page equipes pourrait ne rien afficher si l'API requiert une authentification.


TABLEAU RECAPITULATIF

# Severite Fichier Description
1.3 BUG CERTAIN ConversationSidebar.tsx:77,327,334 searchTimerRef jamais nettoye au cleanup
1.4 BUG CERTAIN ConversationSidebar.tsx:151-156 Polling 500ms infini apres toggle archive
2.1 BUG CERTAIN markdown.ts:46, EventCard.tsx:407,423, history:376 XSS via javascript: URI dans liens markdown
9.1 BUG CERTAIN history/index.tsx:82 window.location.href perd le message envoye
1.5 BUG PROBABLE layout.tsx:167-169 Race condition setTimeout(500) pour load history
2.5 BUG PROBABLE EventCard.tsx, ChatMessages.tsx Types TS incomplets, acces as any partout
5.1 BUG PROBABLE 15+ fichiers Tous les catch silencieux, pas de feedback utilisateur
5.3 BUG PROBABLE layout.tsx:97-105 Pas de gestion des 401/500 sur les fetch REST
6.1 BUG PROBABLE prompts/index.tsx:186-266 Modal sans trap de focus ni ARIA
6.2 BUG PROBABLE Sidebar, EventCard, ChatMessages Boutons SVG sans aria-label
9.5 BUG PROBABLE teams/index.tsx:44 API key manquante dans la requete fetch
1.2 RISQUE THEORIQUE ChatMessages.tsx:22 useStore mute depuis handler vanilla JS
1.6 RISQUE THEORIQUE ConversationSidebar.tsx:201-203 setTimeout(500) pour template message
2.2 RISQUE THEORIQUE 15+ fichiers API key en localStorage sans rotation
2.3 RISQUE THEORIQUE ws-manager.ts:93-101 API key envoyee dans chaque message WS
3.3 RISQUE THEORIQUE EventCard.tsx:174-188 9 signaux par EventCard, centaines d'instances
4.3 RISQUE THEORIQUE ConversationSidebar.tsx:289-297 revokeObjectURL trop rapide
6.3 RISQUE THEORIQUE history, decisions, help, monitor, sidebar Navigation clavier impossible sur les div cliquables
6.4 RISQUE THEORIQUE ChatMessages.tsx:294-310 Pas de live region pour le streaming
7.1 RISQUE THEORIQUE Architecture globale Bus evenementiel non type, fragile

Total : 4 BUG CERTAIN, 7 BUG PROBABLE, 10 RISQUE THEORIQUE


RECOMMANDATIONS PRIORITAIRES

  1. XSS (2.1) — Ajouter une validation href.startsWith('http') dans le regex des liens markdown, ou filtrer les protocoles dangereux (javascript:, data:, vbscript:).

  2. Polling infini (1.4) — Remplacer le setInterval par un watcher reactif ou mettre a jour origShowArchived apres detection.

  3. Timer leak (1.3) — Ajouter clearTimeout(searchTimerRef.value) dans le cleanup de useVisibleTask$.

  4. Message perdu (9.1) — Remplacer window.location.href = '/' par useNavigate()('/').

  5. API key manquante (9.5) — Ajouter l'header d'authentification dans teams/index.tsx.

  6. Feedback utilisateur (5.1) — Ajouter des toasts ou alertes pour les erreurs reseau.

  7. Accessibilite (6.1, 6.2) — Ajouter role="dialog", aria-modal, aria-label sur le modal et les boutons.