CRM API — Référence des endpoints

Arc OS — The Orchestration System for AI Teams

Informations générales

Paramètre Valeur
Base URL https://arc-os.co/api/crm
Autorisation Authorization: Bearer <JWT> ou ?token=<JWT> (pour SSE/WebSocket)
Content-Type application/json
Algorithme JWT HMAC-SHA256
TTL JWT 24 heures

Authentification

Tous les endpoints (sauf /docs/*) nécessitent un token JWT dans l'en-tête Authorization: Bearer <token>.

Pour les connexions SSE et WebSocket, le token est transmis via le query param ?token=<JWT>.

Erreurs d'autorisation

Code Description
401 Token absent ou invalide
403 Accès au projet refusé (multi-tenancy)

Endpoints par catégorie

Compte et paramètres

Méthode Chemin Description
GET /account/settings Récupérer les paramètres du compte
PUT /account/settings Mettre à jour les paramètres du compte

Onboarding + Trial Credits (Phase 50.1)

Méthode Chemin Description
POST /onboarding/setup Crée le premier projet. Body multipart : config (JSON) + files. Le champ anthropicKey est désormais optionnel — si vide + user avec email_verified + n'ayant jamais eu d'essai gratuit, le projet est créé en trial_mode=1 avec 100K free tokens. Response : { ok, project, trial_activated }. Phase 51 : retourne 402 avec {error:"plan_limit_reached", reason, current, limit, plan} quand l'utilisateur dépasse la limite de projets pour son plan.
GET /account/trial-status Statut de l'essai gratuit pour le bandeau UI. Response : { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] }

Onboarding Checklist (Phase 54.1, issue #56)

Checklist d'engagement post-wizard en 5 étapes. Chaque étape (workers, cli, skill, bot, issue) accepte le statut completed ou skipped. Les mutations sont idempotentes : un POST identique répété retourne le même état, sans écrire de doublon dans activity_log. Replay ne réinitialise pas l'état, il efface seulement dismissed_at — l'UI réaffiche le panneau avec le même progression.

Méthode Chemin Description
GET /onboarding/progress État actuel pour l'utilisateur authentifié. Response : { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Utilisateur non touché → zéros/null sans création de ligne.
POST /onboarding/event Enregistre une transition d'étape. Body : { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Validation whitelist → 400 sur étape/statut inconnu. Response : même shape que GET. Émet onboarding_step_completed/onboarding_step_skipped dans activity_log uniquement sur changed ; lors de la transition à 5/5, émet aussi onboarding_completed avec duration_ms.
POST /onboarding/dismiss Ferme le panneau (dismissed_at = now). Idempotent. Émet onboarding_dismissed au premier appel avec payload {completed_count}.
POST /onboarding/replay Rouvre le panneau fermé (dismissed_at = NULL). L'état des étapes n'est pas affecté. Émet onboarding_replayed lors du clear-event.
POST /projects/:name/active-issue Issue #115. Lie la session web actuelle à une issue. Body : { issue_id: number, title?: string }. Écrit un événement session_active_issue dans activity_log (source=web).
GET /projects/:name/active-issue Issue #115. Dernière issue liée pour ce propriétaire dans les 7 derniers jours. Response : { active_issue_id, title, ts }.
GET /onboarding/cli-status Phase 54.3 (issue #58). L'utilisateur s'est-il connecté via arc login ces 30 derniers jours ? Response : { installed: boolean, last_cli_at: string|null }. SSOT — lignes dans activity_log avec event_type='cli_invocation' et actor=chatId. Le checklist d'onboarding frontend poll cet endpoint toutes les 10s tant que l'étape CLI est pending ; quand installed=true — marque automatiquement l'étape cli comme completed.
GET /analytics/onboarding-funnel Phase 54.6 (issue #61). Stats de funnel agrégées sur une fenêtre glissante. Query : hours=168 (1-720, défaut 7j). Response : { hours, total_steps:5, started_users, completed_users, completion_rate, per_step: [{step, completed, skipped}…], duration_p50_ms, duration_p90_ms, ttfc_p50_ms, ttfc_sample_size }. SSOT — événements activity_log onboarding_step_* + onboarding_completed + cli_invocation. TTFC = time-to-first-arc (delta julianday entre le premier onboarding-step et le premier cli_invocation par actor).

Le SSOT pour les métriques de funnel (Phase 54.6 / issue #61) correspond aux événements dans activity_log (event_type LIKE 'onboarding_%'). La table onboarding_progress est un cache dérivé : l'UI se rend en une seule requête plutôt qu'en agrégeant les événements.

Beta Feedback (Phase 53.3)

Méthode Chemin Description
POST /feedback Envoyer un feedback bêta. Body : {type: "bug"|"feature"|"other", title, description, project?, browser?}. Écrit dans activity_log (event_type=feedback_report) et envoie un ping CEO sur Telegram.
GET /admin/feedback Liste des dernières soumissions (admin uniquement). Query : limit=50 (max 500). Response : {items: [...], count}.

POST /feedback — validation du body : type ∈ {bug,feature,other}, title ≤200 chars, description ≤5000 chars. Succès → {ok: true, type, title}. Le ping Telegram est formaté comme 🐞/💡/📝 New <type> feedback ... From: <user> Title: <title> + les 400 premiers caractères de la description.

Le widget flottant dans FeedbackWidget.jsx (CRM dashboard) transmet automatiquement browser (UA + viewport + locale) et project (technical_name du projet actif).

Arc Help AI Chat (Phase 61, #147)

Méthode Chemin Description
POST /help/chat Q&A IA in-app. Body : {message, history: [{role,text}]}. Response : {reply, sources: string[], remaining, limit}. Rate limit : 30/jour/utilisateur.
GET /help/usage Limite courante. Response : {remaining, limit, used}.

POST /help/chat — pipeline : (1) check de rate-limit (429 si dépassé), (2) RAG via shared/rag.ts (Cohere + sqlite-vec, Phase 71) fusionnant les hits projet + skills _global_ → fallback recherche par mots-clés dans docs/public/, (3) Claude Haiku avec prompt système + contexte doc + historique. message ≤2000 chars. Répond dans la langue de la requête.

Beta Invites (Phase 52.1, admin uniquement)

Méthode Chemin Description
GET /admin/wipe-metrics Tableau de bord de télémétrie WIP-E (#308). Admin uniquement. Retourne : {render: {count, avg_ms, p50_ms, p95_ms, max_ms}, interaction: {count, avg_per_session, p95_per_session, max_per_session, sessions_zero}, by_worker: [{worker_id, render_count, avg_render_ms, session_count, avg_interactions}], daily: [{date, renders, interactions, avg_render_ms}], recent: [...]}.
GET /admin/invites Liste de tous les codes d'invitation + compteurs (total_active, total_used). Admin uniquement.
POST /admin/invites Générer N codes. Body : {count: N, note?: string}. Admin uniquement. Response : {ok, codes, count}.
DELETE /admin/invites/:code Révoquer un code d'invitation non utilisé.
/admin/notebooklm/* Supprimés en Phase 71.8 avec le NotebookLM Bridge. La recherche sémantique passe désormais par le RAG self-hosted (rag-architecture.md).

Mise à jour du flux auth : POST /api/auth/register requiert désormais le champ invite_code (bêta fermée Phase 52.1). Sans code → 403 {error: "invite_required"}. Code invalide/utilisé → 403 {error: "invalid_invite"}.

Standard Cloud — WebSocket Terminal + SSE Logs (Phase 60 #139)

Protocole Chemin Description
WS /ws/cloud/:userId/terminal?token=<JWT> Proxy vers docker exec -i <containerId> /bin/bash. IDOR : userId doit correspondre au chatId du JWT. Un container en pause est auto-réveillé. Frames WS entrantes → stdin du container ; stdout+stderr → frames WS.
SSE /api/sse/cloud/:userId/logs docker logs -f --tail 50 pour le container de l'utilisateur. Auth : Bearer JWT. IDOR : userId === chatId. Événements : data: {"line": "..."} par ligne, data: {"closed": true} à la sortie.

Standard Cloud (Phase 60)

Méthode Chemin Description
POST /cloud/claude-verify Vérifie claude --version dans le container (shell-quoted transport-safe via SSH en mode remote-host, #329). Définit claude_authed=true. Response : { ok, output }
POST /cloud/ssh-keygen Génère une clé ed25519 dans le container (idempotent). Response : { public_key }
POST /cloud/ssh-verify ssh -T [email protected] dans le container. Définit github_authed=true en cas de succès. Response : { ok, output }
POST /cloud/provision Provisionne un container Docker pour l'utilisateur. Exige le plan cloud, 402 sinon. Idempotent : si le container existe déjà — retourne l'état courant. Response : { container_id, status, server_ip, port, claude_authed, github_authed }
GET /cloud/status État du container + réconciliation live docker inspect. Response : { container_id, status, server_ip, internal_port, claude_authed, github_authed, docker_running, last_active, created_at } ou { status: "none" }
POST /cloud/deprovision Arrêter + supprimer le container (docker stop + docker rm -f + docker network rm arc-net-{id}). Met status=deleted en DB. Response : { ok: true, container_id }

Statuts du container : provisioningreadypausedsuspended / deleted.

Sécurité (SEC-60 #152, #154, #155, #156) : chaque container est isolé dans son propre réseau arc-net-{id} (prévention du lateral movement). Connexion SSH Contabo→Hetzner via l'utilisateur dédié arcapi (groupe docker, sans root) avec wrapper docker-only — les commandes non-docker sont bloquées au niveau de authorized_keys. ARC_TOKEN est injecté via docker exec après démarrage (invisible dans docker inspect). git clone limité par timeout 60. Idle timeout WebSocket : 120 s. SSE docker logs limité à --since 1h. Prévention IDOR : tous les endpoints vérifient container.user_id === req.userId. Flags de sécurité au docker run : --cap-drop=ALL --security-opt=no-new-privileges --cpus=1.5 --memory=2g --pids-limit=200. Volumes : arc-{id}-workspace:/workspace, arc-{id}-claude:/home/arcuser/.claude, arc-{id}-ssh:/home/arcuser/.ssh. Lifecycle (#141) : GET /cloud/status met toujours à jour last_active. Idle 30 min → docker pause (cron toutes les 5 min, scripts/cloud-lifecycle-cron.ts). Réveil : message CRM, message TG, upgrade WS → docker unpause automatique.

Waitlist (#134) :

Méthode Chemin Description
POST /cloud/waitlist Rejoindre la file. Idempotent. Response : { position, status, joined_at, message }. 409 si déjà sur un plan cloud ou si un container existe déjà.
GET /cloud/waitlist/status Propre statut dans la file. Response : { position, status, joined_at, invited_at } ou { status: "not_joined" }.
GET /cloud/waitlist Admin uniquement. Liste complète + stats. Response : { stats: { total, waiting, invited, activated }, list: [...] }.
POST /cloud/waitlist/invite Admin uniquement. Inviter un utilisateur. Body : { user_id }. Définit status=invited + upgrade automatique du plan vers cloud. Response : { ok, user_id, position }.

Billing (Phase 51 → #202 Plata by mono)

Phase #202 : Stripe est remplacé par Plata by mono (acquiring internet monobank). Abonnements récurrents via tokenization (la carte est enregistrée au premier paiement).

Méthode Chemin Description
GET /account/usage Historique d'utilisation des tokens pour l'utilisateur autorisé (Phase 63, #148). Réponse : { rows: [ { project_name, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at } × jusqu'à 200 ], totals: { total, input, output } }. Lit token_usage_log par owner_id. Affiché dans UserDropdown (UsageCard) et BillingPage (section Token Usage).
GET /account/billing-summary Résumé billing consolidé (#309). Response : { arc: { plan, status, tokens_this_month, tokens_input, tokens_output, renewal_date }, anthropic: { connected, key_prefix, credit_balance_usd, spend_month_usd, tokens_this_week } }. La section Anthropic est remplie côté serveur via l'API Anthropic avec la clé account_settings.anthropic_key de l'utilisateur (fallback → PLATFORM_ANTHROPIC_KEY). Affiché dans le UsageCard du UserDropdown.
GET /billing/status Plan actuel, limites, usage, features. Response : { plan, status, current_period_end, next_billing_date, plata_masked_pan, limits, usage, features, pricing, can_upgrade, plata_ready }
POST /billing/checkout-session Crée une invoice Plata avec tokenization. Body : { plan: "min"|"cloud", success_url?, cancel_url? }. Response : { url, invoice_id, plan, amount_uah }. 503 si PLATA_MERCHANT_TOKEN n'est pas dans le vault.
POST /billing/webhook Callback Plata (PAS d'auth CRM — vérifié par le header X-Token). Statuts : success (active le plan + enregistre le cardToken), failure/expired (incrémente billing_failures, 3+ → downgrade vers free). Idempotent via la table plata_events.
POST /billing/cancel Annuler l'abonnement (downgrade vers free). Met en pause le container Docker pour le plan cloud. Response : { ok, plan: "free" }.

#205 (2026-05-26) : la route legacy /billing/portal-session a été supprimée avec le dead code Stripe. Utilise /billing/cancel pour annuler un abonnement.

Limites du plan (sémantique OR) :

Réponse 402 sur POST /onboarding/setup ou POST /projects/:name/workers quand la limite est dépassée : { error: "plan_limit_reached", reason: "projects_limit"|"workers_limit", current, limit, plan, message }

Les utilisateurs admin (role=admin) contournent entièrement la vérification des limites de plan — ce sont des opérateurs, pas des tenants payants.

Les bêta-testeurs (subscriptions.plan='beta', Phase 52 F&F) contournent aussi — nombre illimité de projets/workers plus toutes les features Max. Assigné manuellement : UPDATE subscriptions SET plan='beta' WHERE user_id=?.

Bugfix (issue #25) : POST /projects/create (Quick Start, Phase 50.2) plantait avec ownerChatId is not defined à cause d'une typo — corrigé, l'audit-actor est désormais correctement enregistré.

Bugfix (issue #26) : allocatePort() pour les nouveaux projets sonde désormais les vrais bindings TCP (ss -tln), et non plus uniquement le registry. Auparavant, il pouvait retourner un port occupé par un service hors-registry (NotebookLM bridge :19213, internal bridges) → le workspace bot plantait sur EADDRINUSE.

Flux auth (Phase 50.1) : /api/auth/register et /api/auth/login retournent désormais un JWT même pour un email non vérifié + flag needs_verification: true. Les actions sensibles (trial grant, billing, invites) vérifient email_verified séparément. Rate limit sur l'inscription : 3 / IP / 24h.


Projets (9 endpoints)

Méthode Chemin Description
GET /projects Liste des projets de l'utilisateur
POST /projects/create Crée un projet
POST /projects/create-with-team Création atomique projet + workers + (opt.) bot TG en une seule requête — body : {project, workers[], telegram?} ; rollback en cas d'erreur
GET /projects/suggest-preset Suggestion de preset par niche — query : niche=<text> ; retourne {preset_id} sur la base d'une keyword map
GET /projects/:name Détails du projet
GET /projects/:name/config Configuration du projet
PUT /projects/:name/config Mettre à jour la configuration
GET /projects/:name/protocol Protocole du projet
PUT /projects/:name/protocol Mettre à jour le protocole
GET /projects/:name/logs Logs du projet
GET /projects/:name/metrics Métriques du projet

POST /projects/create — body :

{
  "technical_name": "string",
  "displayName": "string",
  "description": "string",
  "icon": "string",
  "color": "string"
}

GET /projects/:name/logs — query : category, lines

GET /projects/:name/metrics — query : since, until


Workers (11 endpoints)

Méthode Chemin Description
GET /workers #304 Phase A — tous les workers de tous les projets de l'utilisateur courant. Response : { workers: [{ id, label, icon, type, model, tools, context_assets, project_name }] }. Filtrage par owner_id (multi-tenancy). Le CEO voit tous les projets.
GET /workers/presets #228 — bibliothèque globale de presets (project-agnostic). Retourne 13 workers du config/workers_registry.json canonique : { presets: [{ id, label, icon, type, model, max_turns, tools, system_prompt, context_assets, focus_dirs, prompt_style }] }. Utilisé par le WorkerCreationWizard pour le Step 1.
GET /workers/templates #304 Phase I — templates de l'utilisateur courant. Response : { templates: [{ id, name, description, config, is_public, created_at }] }.
POST /workers/templates #304 Phase I — enregistrer/mettre à jour un template. Body : { name, description?, config }. Response : { ok, id }.
DELETE /workers/templates/:id #304 Phase I — supprimer un template (propriétaire uniquement). Response : { ok }.
GET /projects/:name/workers Liste des workers
POST /projects/:name/workers Crée un worker
POST /projects/:name/workers/reorder Phase 53.8 — réordonner les workers. Body : {order: [id1, id2, ...]}. Réécrit workers_registry.json de façon atomique. Les workers absents de order sont ajoutés à la fin (protection contre la perte). Response : {ok, count, order}.
PUT /projects/:name/workers/:id Mettre à jour un worker
DELETE /projects/:name/workers/:id Supprimer un worker
POST /projects/:name/workers/generate-prompt Générer un prompt système
GET /projects/:name/workers/:id/telegram-token Récupérer le token Telegram
POST /projects/:name/workers/:id/telegram-token Phase 53.4 — valide le token via Telegram getMe, stocke bot_username dans le vault, refuse si le même bot est déjà lié à un autre worker (409). Response : {ok, started, bot_username}.
DELETE /projects/:name/workers/:id/telegram-token Supprimer le token Telegram
POST /projects/:name/workers/:id/avatar #304 Phase D — uploader un avatar (multipart file, JPEG/PNG/WebP, max 2 MB). Vérification magic-byte. Stocke dans data/worker-avatars/, enregistre dans worker_avatars (migration 043). Response : { ok, url }.
GET /projects/:name/workers/:id/avatar #304 Phase D — récupérer l'avatar en binaire (Content-Type selon le MIME). 404 si aucun avatar uploadé.
DELETE /projects/:name/workers/:id/avatar #304 Phase D — supprimer l'avatar, réinitialiser avatar_pack='role' dans le JSON du worker.
GET /projects/:name/workers/:id/activity #306 — feed d'activité du worker (50 derniers événements). Fusion : activity_log (actor=workerId) + project_issues.activity (author=workerId) + token_usage_log (snapshots journaliers). Response : { events: [{ type, title, detail, when }] }. Types : git_commit, skill_loaded, skill_unloaded, issue_pick, issue_close, issue_log, token_budget, session_start.
GET /projects/:name/workers/:id/runtime #306 — état runtime du worker. Response : { status: 'working'|'idle', status_started_at, tokens_today, tokens_pct, tokens_cap, current_skill }. Lit d'abord workers_runtime_state (migration 045) ; fallback de péremption : status='working' + tmux mort + updated_at > 10 min → idle (détection de crash). Plafond journalier selon le plan via lookup subscriptions.plan : free=100K, starter=400K, starter_cloud=2M, beta=sans compteur (retourne tokens_cap: null, tokens_pct: 0). Intervalle de poll 15 s.
POST /projects/:name/workers/:id/notify Phase 53.2 — envoyer un ping d'événement TG ({event?, text, buttons?}). Silent no-op si aucun token lié ou CRM_DISABLE_TG_NOTIFY=1.
POST /projects/:name/workers/:id/suggest-bot-username 53.11.1 (issue #48) — retourne 5 candidats de username TG pour le wizard de création de bot au format <project>_<worker>_bot + fallbacks numérotés. Slugify supprime les tirets, troncature à 32 chars (la partie worker est tronquée en premier). Response : {candidates: string[]}.
POST /metrics/wizard 53.11.1 (issue #48) — sink de télémétrie pour le wizard de création de bot. Body : {action, duration_ms?, attempts?, success?, project?, worker_id?, locale?} (#124 : événements locale_active/locale_switch). Écrit dans activity_log (event_type=wizard_metric), best-effort.
GET /analytics/wizard-metrics?hours=168 53.11.1 (issue #48) — résumé du funnel : {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Défaut 7 jours, clamp 1-720h.
POST /projects/:name/restart Redémarrer un worker
GET /projects/:name/active-role Rôle actif actuel
POST /projects/:name/active-role Changer le rôle actif

POST /projects/:name/workers — body :

{
  "label": "string",
  "icon": "string",
  "type": "terminal | telegram",
  "model": "string",
  "max_turns": 20,
  "tools": ["Read", "Write", "Bash"],
  "system_prompt": "string",
  "focus_dirs": ["src/", "docs/"]
}

max_turns vaut 20 par défaut (auparavant 5, ce qui provoquait l'erreur "Reached max turns" dans les dialogues multi-étapes avec tool calls).

POST /projects/:name/restart — query : worker_id


Fichiers et stockage (8 endpoints)

Méthode Chemin Description
GET /projects/:name/files Arborescence des fichiers
POST /projects/:name/files/upload Envoyer un fichier (multipart, max 100 Mo)
POST /projects/:name/files/mkdir Crée un répertoire
POST /projects/:name/files/create Crée un fichier
GET /projects/:name/files/read Lire un fichier
PUT /projects/:name/files/save Enregistre un fichier
DELETE /projects/:name/files/delete Supprimer un fichier
POST /projects/:name/files/clone Git clone d'un dépôt

GET /projects/:name/files — query : path

GET /projects/:name/files/read — query : path, raw


Skills (18 endpoints)

Skills du projet

Méthode Chemin Description
GET /projects/:name/skills Liste des skills du projet
POST /projects/:name/skills Crée une skill
PUT /projects/:name/skills/:id Mettre à jour une skill
DELETE /projects/:name/skills/:id Supprimer une skill

#210 (2026-05-26) : la DB (skills_global) est désormais le writer SSOT. Les sauvegardes UI vont d'abord en DB ; .claude/skills/<name>/SKILL.md est écrit en write-through comme artefact pour que le CLI Claude Code auto-découvre les skills. Les écritures legacy skills/<name>.md ont été supprimées — les fichiers existants ne sont plus lus ni maintenus. Helper de migration : scripts/migrate-skills-to-db.ts.

Marketplace global

Méthode Chemin Description
GET /skills Liste des skills globales
POST /skills Publier une skill
GET /skills/:id Détails d'une skill
PUT /skills/:id Mettre à jour une skill
DELETE /skills/:id Supprimer une skill

Évolution et mises à jour

Méthode Chemin Description
GET /skills/:id/evolution Historique d'évolution d'une skill
GET /skill-updates Liste des mises à jour disponibles
POST /skill-updates/:id/approve Accepter une mise à jour
POST /skill-updates/:id/reject Rejeter une mise à jour

Forks de skills

Méthode Chemin Description
GET /projects/:name/skill-forks Liste des forks
POST /projects/:name/skill-forks Crée un fork
PUT /projects/:name/skill-forks/:id Mettre à jour un fork
DELETE /projects/:name/skill-forks/:id Supprimer un fork

Chat et messages

Méthode Chemin Description
POST /projects/:name/chat Envoyer un message dans le chat
GET /projects/:name/chat/history Historique du chat
POST /projects/:name/message Envoyer un message à un worker (Phase 48.6 : wake-up automatique du worker idle-killed, ~2-4s cold start ; Phase 48.6.1 : le wake-up fonctionne aussi dans les projets single-mode, pas seulement parallel)
GET /projects/:name/pins Liste des notes (pins)
POST /projects/:name/pins Crée une note
DELETE /projects/:name/pins/:id Supprimer une note

Wiki (4 endpoints)

Méthode Chemin Description
GET /projects/:name/wiki/tree Arborescence des pages wiki
GET /projects/:name/wiki/file Lire une page wiki
PUT /projects/:name/wiki/save Enregistre une page wiki. Phase 71.5 : déclenche syncWiki → re-embed Cohere (fire-and-forget ; les échecs sont loggés, l'écriture ne plante pas).
GET /projects/:name/wiki/download Télécharger le wiki en archive ZIP

Analytique (4 endpoints)

Méthode Chemin Description
GET /analytics/activity Fil d'activité
GET /analytics/sidebar Données pour le panneau latéral
GET /analytics/phases Liste des phases du projet
POST /analytics/phases Mettre à jour les phases du projet

Marketplace et Sage (8 endpoints)

Méthode Chemin Description
GET /sage/scout/categories Catégories du marketplace
POST /sage/scout Rechercher des skills
POST /sage/scout/quick-scan Scan rapide
POST /sage/scout/analyze Analyse approfondie d'une skill
POST /sage/scout/install Installer une skill
POST /sage/analyze Analyse Sage
GET /sage/status Statut du service Sage
POST /sage/benchmark Lancer un benchmark

Mémoire et Knowledge

Méthode Chemin Description
GET /projects/:name/rag/search?q=...&k=6&include_global=true&doc_types=wiki,issue,skill,transcript Phase 71.7 (#364) : recherche sémantique sur embeddings + embeddings_vec (Cohere + sqlite-vec). Paramètres : q (texte de la requête), k (1-25, défaut 6), include_global (défaut true — fusion avec le namespace de skills _global_), doc_types (sous-ensemble séparé par virgules ; type supplémentaire Phase 73.6 : transcript). Response : { query, project, hits: [{rank, doc_type, doc_id, chunk_ix, distance, scope: 'project'|'global', text}] }. Alimente arc kb search + l'outil de chat ask_notebooklm. Architecture : rag-architecture.md.
POST /projects/:name/memory/refresh Phase 71.8 (#365) : re-embed MANIFEST + ROADMAP + fichiers clés dans le store RAG (auparavant — sync vers NotebookLM). Même endpoint, nouvelle sémantique.
POST /projects/:name/memory/fetch-artifact Supprimé en Phase 71.8 (l'audio overview n'a pas d'équivalent RAG) — retourne 410 Gone.
GET /projects/:name/learnings Liste des learnings
POST /projects/:name/learnings Ajouter un learning
GET /projects/:name/knowledge-graph Graphe de connaissances du projet

Documentation (globale, sans auth)

Méthode Chemin Description
GET /docs/tree?lang=<lang> Arborescence de la documentation ; lang optionnel (en/uk), défaut en
GET /docs/file?path=<p>&lang=<lang> Lire un fichier de documentation avec language fallback

GET /docs/tree — query : lang (optionnel)

GET /docs/file — query : path (obligatoire), lang (optionnel)


Système

Méthode Chemin Description
GET /system/configs Récupérer les configurations système
PUT /system/configs Mettre à jour les configurations système

Codes d'erreur

Code Signification
200 Succès
201 Créé
400 Requête invalide
401 Non autorisé
403 Interdit (multi-tenancy)
404 Non trouvé
409 Conflit (doublon)
429 Trop de requêtes
500 Erreur serveur

GitHub Integration (Phase 49.3)

Endpoint Méthode Description
/api/crm/projects/:name/github GET Liste des repos GitHub liés au projet
/api/crm/projects/:name/github POST Lier un repo (body : {owner, repo}) — retourne webhook URL + secret + instructions de setup
/api/crm/projects/:name/github/:id DELETE Délier un repo
/api/crm/projects/:name/github/events GET Liste des derniers événements GitHub (Phase 49.3.1, query : ?limit=50)
/api/webhooks/github POST Récepteur de webhook public (validé HMAC-SHA256, rate-limit 100/min)

Événements supportés : push, pull_request, workflow_run, issues. Notifications routées vers le Telegram du propriétaire du projet.

Account Security (Phase 45.4)

Endpoint Méthode Description
/api/crm/account/recovery GET Liste des recovery keys actives
/api/crm/account/recovery POST Crée une recovery key (body : encryptedKey, keyHint)
/api/crm/account/recovery DELETE Révoquer une ou plusieurs recovery keys (body : { id } ou {} pour toutes)
/api/crm/account/recovery/restore GET Récupérer la clé maître chiffrée pour la restauration

Sécurité


Phase 53.13 — baseline type-safety (2026-05-10)

Aucun changement de comportement des endpoints — uniquement des types internes. tsc --noEmit bloque désormais push/CI :


Sentinel Pentest Remediation (2026-06-10, #433–#444)

Sprint de pentest white-box — changements de comportement des endpoints après correction de 3×P1 + 4×P2 + 3×P3 :


Phase 53.15 — Sentinel Sprint 1 (2026-05-10)

Changements de comportement des endpoints auth + admin (corrections P0 de l'audit Sentinel) :


Phase 53.21 — Sentinel P2 batch 2 (2026-05-12)

Phase 53.18 — correction fuite secret tmux (2026-05-11)

Aucun changement de comportement des endpoints — uniquement un refactor des chemins de spawn internes.

Phase 53.16 — Sentinel Sprint 2 (2026-05-10)

Changements de comportement des endpoints après le hardening 13 × P1 :

Phase 55 — Cosmic Editorial login (2026-05-13)

Nouveaux endpoints pour la connexion via magic-link :

L'union EphemeralTokenType est étendue : elle contient désormais "magic_link" aux côtés des types existants oauth_state / password_reset / email_verification / tfa_challenge.

Le frontend (CosmicCard.jsx) gère l'état magic (countdown de renvoi de 60 s) et le paramètre URL ?magic_token= (auto-consume → login → animation de succès).

Phase 56 — AI Interop / Project Context Export (2026-05-13)

Export réservé au propriétaire d'un snapshot sanitisé du projet en .md pour transmission à un AI externe (Gemini / ChatGPT / Perplexity / Claude.ai).

Alerte : quand le propriétaire dépasse 3 exports en 24h ET prefs.notify_on_export = true (défaut OFF) — logActivity("export_alert", ...) transite par le pipeline de notification TG Phase 53.10 existant (alertFired: true dans le corps de la réponse).

Scanner multi-niveaux (shared/secret-scanner.ts) — Tier 1 regex (PATTERN_REGISTRY depuis le sanitiseur PII), Tier 2 entropie Shannon ≥4,5 bits/char sur des séquences ≥20 chars, Tier 3 heuristiques contextuelles (key=/token:/secret=/password=). Whitelist : UUID / git SHA / SHA-256 / caractères répétés / hex court / base58 basse entropie. Niveaux de sévérité (critical/high/medium/low). Performance : <500 ms / 1 Mo.

Migration DB 024 — tables export_audit_log + export_preferences.


Phase 57 — Platform Settings (suivi Sentinel #103, 2026-05-15)

Gestion des secrets super-admin via l'UI CRM au lieu de ssh/edit-.env/paste-in-chat. MVP backend (Stage 1 sur 4 stages). Tous les endpoints sont gateés par requireAdmin (Phase 53.15) — retournent 403 Forbidden — admin only pour les non-admin, 401 Unauthorized sans JWT.

Liste d'exclusion stricte NEVER_EXPOSE : CRM_SECRET (signature JWT) + SECRET_ENCRYPTION_KEY (méta-clé vault) — même une requête admin avec un token valide retourne 400 "not managed". Le log d'audit est en append-only (pas de handler UPDATE/DELETE), chaque action (y compris les échecs) écrit une ligne avec IP + UA + email.

Migration DB 026 — table platform_audit_log. Stage 2 (frontend PlatformSettings.jsx) — livré 2026-05-15 (cbc8bac) : grille de cartes admin uniquement + modal de rotation (<input type="password"> + confirmation de saisie) + drawer d'audit ; entrée dans la sidebar filtrée par userRole === "admin" récupéré depuis /api/auth/me.

Polish (2026-05-15, commit 56191b0) — Restructuration UI Platform Settings. Les items de la réponse GET /api/crm/platform/settings gagnent 5 nouveaux champs : category (anthropic|oauth|telegram|email), usedIn (string[] — fichiers/flux qui consomment la clé), getFromUrl (où obtenir une nouvelle valeur), effectAfterRotate, riskIfLeaked. Utilisés par le frontend pour afficher 4 groupes de cartes par section + panneau d'aide repliable par carte avec contexte structuré (Used in / Get from / Effect / Risk). Aucun changement de comportement des endpoints mutateurs (PUT/POST/restart/test).

Refactor (2026-05-16) — nettoyage interne de shared/routes/platform.ts. 39 lignes supprimées (16 ajoutées), aucun changement de surface API publique. Les signatures et réponses des endpoints PUT/POST/restart/test/audit sont inchangées. Documenté ici uniquement parce que le hook pre-push de couverture doc se déclenche sur tout diff shared/routes/*.ts.

Activité antidatée (#117, 2026-05-16)POST /api/mcp/issues/:project/:id/log accepte désormais un champ ts optionnel (chaîne ISO-8601). Utilisé par la reconstruction arc retro pour que les entrées historiques atterrissent à leur timestamp d'origine. Les valeurs à date future sont silencieusement plafonnées à now dans addActivity() (défense contre les erreurs de saisie). ISO invalide → 400.

Stage 3 (2026-05-15) — hot-reload des secrets OAuth + Resend sans restart. shared/auth.ts loadOAuthConfig() lit désormais getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") à chaque appel au lieu de process.env. Les callsites dans master-bot/routes/auth.ts appelaient déjà getOAuthConfig() par requête → 0 changement de callsite. RESEND_API_KEY était déjà hot-reload via shared/email.ts:47. Changement de comportement : PUT /api/crm/platform/settings/{GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GOOGLE_CLIENT_ID|GOOGLE_CLIENT_SECRET|RESEND_API_KEY} prend désormais effet dès la prochaine requête, sans restart requis. restartTargets pour ces 5 clés est vide → le bouton Restart est masqué dans l'UI. Cas limite : un flux OAuth avec un state-token émis avant la rotation peut recevoir un 400 au callback lors du code-exchange — un retry utilisateur résout le problème. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN restent restart-required (lus lors du spawn du child-bot / initialisation du long-poll TG).

Nettoyage Phase 57.3.5 (2026-05-16) — l'allowlist MANAGED_KEYS réduite de 9 à 6. Supprimés : ANTHROPIC_API_KEY (les opérateurs utilisent désormais PLATFORM_ANTHROPIC_KEY pour les trial-credits et l'inférence plateforme ; le fallback .env fonctionne toujours pour les chemins de code legacy jusqu'à la migration de Sage/Karpathy), CITADEL_BOT_TOKEN (le bot par projet appartient aux entrées vault child:<name>:token, géré par le flux d'onboarding worker — pas les Platform Settings). MASTER_BOT_TOKEN remis à jour : label → "Telegram — System Monitor Bot", description → "Server health alerts + on-demand status probes (admin-only, not a chat bot)". La Phase 58 ajoutera la boucle de monitoring (push alerts pour crash worker / disque / RAM / brute-force SSH / bypass CF + commandes /status, /health, /errors, /restart). Set final : PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (refs #103).

Phase 63 — Consolidation UI/UX + Suivi de l'utilisation des tokens (2026-05-21, #148)

Nouvel endpoint :

Modifications dans claude-runner.ts :

Modifications UI (pas API) :

Arc Help (Phase 61 / #147)

Historique (Phase 61 / #153) :

RGPD / Conformité (Sprint 1+2, #161–#174, 2026-05-22)

Droit à l'effacement — DELETE /api/auth/account (#162)

Supprime définitivement l'utilisateur authentifié et toutes ses données (RGPD Art. 17).

Password Version / Invalidation des tokens (#174)

La migration 035 ajoute password_version INTEGER NOT NULL DEFAULT 0 à users. Au changement de mot de passe, password_version est incrémenté. Le payload JWT inclut le champ pv. crmAuthMiddleware valide pv contre la DB à chaque requête et rejette les tokens émis avant le dernier changement de mot de passe (401 "Token invalidated — please log in again"). Fail-open si la DB est indisponible.

Cron de rétention des données (#168)

Le master bot lance une purge quotidienne au démarrage + toutes les 24 h. Limites de rétention : chat_messages 180 jours (par timestamp), activity_log 365 jours (par created_at), auth_events 90 jours (par ts), token_usage_log 730 jours (par created_at unixepoch), export_audit_log 365 jours (par exported_at). Non fatal — l'effacement ne bloque pas le démarrage.

Conformité email (#167)

Tous les emails transactionnels sortants (réinitialisation de mot de passe, vérification, magic-link) incluent désormais :

Sécurité — Vérification HIBP des mots de passe compromis (#171)

Sur POST /api/auth/register et POST /api/auth/reset-password, le mot de passe soumis est vérifié contre l'API k-anonymity de HaveIBeenPwned avant stockage. Seuls les 5 premiers caractères hex du hash SHA-1 sont envoyés à HIBP — le mot de passe complet ne quitte jamais le serveur. Si le mot de passe apparaît dans une base de fuites avec count > 0, la requête est rejetée avec HTTP 400 : "This password was found in a known data breach. Please choose a different password." Fail-open en cas de timeout/erreur HIBP (timeout 4 s) — un HIBP down ne bloque pas l'inscription.

Portabilité des données — GET /api/auth/export (#163)

RGPD Art. 20 — Droit à la portabilité des données. Retourne un fichier JSON structuré contenant toutes les données personnelles qu'Arc OS détient sur l'utilisateur authentifié.

Arc Help — Prompt système durci + anti-injection (#151)

Changements de comportement de POST /api/crm/help/chat (aucun changement de surface API) :

Durcissement de la discipline des workers (#187, #188, #189, 2026-05-23)

Extension des statuts d'issue (#187)

PUT /api/mcp/issues/:project/:id accepte désormais des valeurs de statut étendues :

Statut Signification
open Pas encore commencé
in_progress Travail en cours (défini par arc issue take)
blocked En attente d'une dépendance externe
deferred Reporté (auparavant stocké uniquement en texte)
closed Terminé

Nouveau champ assignee : les issues ont désormais assignee: string | null. Défini via arc issue take <id> ou --assignee <worker_id> dans arc issue update.

Migration 036 : ALTER TABLE project_issues ADD COLUMN assignee TEXT (nullable, auto-appliquée au démarrage du serveur).

Commande CLI arc issue take <id> (#187)

Raccourci pour s'approprier une issue : définit assignee = current_worker_id, status = in_progress, logge l'activité, écrit l'état de session. Équivalent à :

arc issue update <id> --status in_progress --assignee developer
arc issue log <id> "Taken by developer — status set to in_progress"

Validation du hook commit-msg (#187)

.githooks/commit-msg valide désormais les issues #N référencées contre le issues/issues.json local :

Injection bridge de PROJECT_MANIFEST.md (#188)

handleCliInit (shared/cli-routes.ts) lit désormais PROJECT_MANIFEST.md à la racine du projet et l'injecte dans le bloc CITADEL sous ## Project Context. Limite : 8000 chars. Cela donne aux workers bridge (qui tournent sur les machines clientes via arc) un accès compact à l'architecture, aux patterns de sécurité, à la structure des fichiers et aux learnings clés du CLAUDE.md complet.

Placement : après PROJECT_RULES.md, avant la liste des skills.

Champ de config worker context_assets (#189)

La config worker dans workers_registry.json supporte un champ optionnel context_assets: string[] — liste de noms de skills automatiquement injectés dans chaque session bridge de ce worker (sans nécessiter arc skill <name>) :

{
  "id": "developer",
  "context_assets": ["crm-api-reference", "archivist_system"]
}

Le contenu de chaque skill est injecté sous ### Auto-Loaded Skills → #### Skill: <name>, tronqué à 3000 chars chacun.

Phase 62 — Saisie vocale (#373, 2026-06-05)

Transcription vocale en temps réel proxifiée via le serveur whisper.cpp self-hosted (arc-whisper.service, port 19214, modèle ggml-base préchargé).

POST /api/crm/voice/transcribe (#373, Phase 62.4)

Transcrit de courts clips vocaux (dictée dans le chat). Proxifie l'audio vers le whisper-server local et retourne le texte.

Auth : token Bearer (ou query ?token=).

Body : multipart/form-data

Champ Type Notes
audio Blob webm / ogg / wav. Max 25 MB.
locale string BCP-47, p. ex. uk-UA, en-US. Passé comme paramètre language à whisper.

Response 200 :

{ "transcript": "Що ти зробив вчора?" }

Codes d'erreur :

Code Signification
400 Champ audio ou locale manquant
413 Audio au-delà de 25 MB
429 Quota journalier atteint (60 min/utilisateur/jour) OU serveur occupé (max 2 transcriptions simultanées)
502 whisper-server a retourné non-200
500 Échec inattendu

Rate limit : voice_usage_log (migration 051) suit les secondes approximatives par (utilisateur, jour) en utilisant la taille de l'upload comme proxy (suppose un codec voix ~32 kbps, précision ±30 %). Plafond dur : 3600 s / jour. Les requêtes qui dépasseraient le plafond retournent 429 avant transfert à whisper.

Note d'architecture : whisper tourne uniquement sur Contabo (pas dans les conteneurs Hetzner par utilisateur). Les octets audio ne quittent jamais Contabo ; le texte résultant est ce que voit le routage cloud-chat de la Phase 70. arc-whisper.service garde le modèle ggml-base préchargé, donc le coût par appel est de l'inférence pure (~3,4 s à chaud pour 11 s d'audio, 3,1× temps réel sur la machine EPYC 6 vCPU actuelle).


Phase 73 — Transcription + analyse de réunions (#377-#384, 2026-06-05)

Upload d'audio/vidéo de réunion vers un projet, transcription whisper + résumé Claude, avec embedding optionnel dans le RAG. Toutes les routes sont protégées par canAccessProject (propriétaire ou admin).

POST /api/crm/projects/:name/transcripts/upload (#377, Phase 73.1)

Upload multipart, retourne 202 avec transcript_id + job_id + status:'queued'. Le job est pris en charge par la file in-process (max 1 simultané).

Champs du body :

Limites : upload max 1 GB, allow-list MIME (mp3/wav/m4a/aac/ogg/opus/flac + mp4/mov/webm/mkv).

Erreurs : 400 (champ manquant / mauvais MIME), 401, 413 (au-delà du plafond), 500 (écriture disque).

GET /api/crm/projects/:name/transcripts (#379, Phase 73.3)

Liste des transcripts du projet, paginée par curseur. Query : ?limit=20&cursor=<id>. Retourne {items: TranscriptSummary[], next_cursor: number|null}.

GET /api/crm/projects/:name/transcripts/:id (#379)

Ligne complète incluant transcript_text, summary_json (parsé en objet) et frames_json (parsé depuis la Phase 73.4).

GET /api/crm/projects/:name/transcripts/job/:jobId/progress (#379)

Flux SSE de la progression du job. Pousse event: progress avec {status, progress_pct, step_label, error} à chaque changement de champ, plus des heartbeats : keep-alive toutes les 1 s pour que le idleTimeout de 10 s de Bun ne tue pas les longs runs whisper. Se ferme avec event: end une fois le statut terminal.

Auth : l'EventSource du navigateur ajoute ?token=<bearer> (impossible de définir le header Authorization).

Statuts terminaux : done (après l'embed RAG Phase 73.6 + nettoyage des fichiers), failed. Note : summarized est une étape transitoire — le SSE reste ouvert pendant embeddingdone. Le bouton d'envoi du frontend se débloque à summarized (n'attend pas le RAG).

Machine à états (Phases 73.1-73.6)

queued
  → extracting_audio    (ffmpeg → WAV mono 16 kHz)
  → transcribing        (whisper-cli -t 4)
  → (vidéo) extracting_frames → frames_extracted   (ffmpeg scene-change)
            → vision_analyzing → vision_analyzed   (Phase 73.4 Claude vision par frame)
  → (audio) transcribed
  → summarizing         (Claude Sonnet → summary_json, reçoit les frames vision comme contexte)
  → summarized
  → embedding           (Phase 73.6 upsert Cohere via shared/rag.ts, sauté si embed_to_rag=0)
  → done                (fichier source + dossier frames supprimés — décision CEO D4)

Forme JSON des frames vision (Phase 73.4, #380)

Stocké comme chaîne JSON dans transcripts.frames_json (reparsé en objet par GET /transcripts/:id).

[
  { "ts_ms": 3000, "description": "Slide titled 'Q3 Revenue' with bar chart showing 30% growth." },
  { "ts_ms": 6000, "description": "Architecture diagram with three boxes labeled API/Worker/DB." }
]

Plafond dur MAX_FRAMES=50 par transcript (~0,15 $ au pire au tarif vision Sonnet typique). Les frames au-delà du plafond sont abandonnées silencieusement, la dernière description conservée reçoit un suffixe [+N more frames dropped]. Les échecs par frame deviennent des chaînes [vision failed: <msg>] — ils n'interrompent pas la passe. Les frames décrites comme "No informational content" sont des frames webcam-only ou décoratives.

Forme JSON du résumé (Phase 73.5, #381)

Stocké comme chaîne JSON dans transcripts.summary_json. Reparsé en objet par GET /transcripts/:id.

{
  "tldr": "1-2 sentence executive summary",
  "key_points": ["..."],
  "action_items": [{"task": "...", "owner": "name or null"}],
  "decisions": ["..."],
  "topics": ["..."],
  "model": "claude-sonnet-4-5",
  "generated_at": "2026-06-05T20:04:15.573Z"
}

La résolution de la clé Anthropic reflète shared/worker-spawn.ts : BYOK account_settings.anthropic_key (déchiffrée si chiffrée), fallback PLATFORM_ANTHROPIC_KEY pour les propriétaires en trial-mode. Les échecs de résumé sont non fatals — transcript_text reste intact, le statut revient à transcribed/frames_extracted pour que l'utilisateur puisse réessayer après avoir corrigé sa clé.

Phase 78 — Notes : Knowledge Collections (#394–#404, 2026-06-08)

Notes par projet façon NotebookLM. Chaque note est une collection de sources (vidéo, audio, YouTube, web, PDF, DOCX, TXT, image) avec un index RAG partagé et un chat.

GET /api/crm/projects/:name/notes

Retourne toutes les notes du projet. Auth requise + canAccessProject.

Response 200 :

[{ "id": 1, "title": "Sprint planning", "description": null, "created_at": "...", "source_count": 3 }]

POST /api/crm/projects/:name/notes

Crée une nouvelle note.

Body : { "title": "string", "description": "string?" }
Response 201 : { "id": 1, "title": "Sprint planning" }

GET /api/crm/projects/:name/notes/:id

Détail d'une note avec sources, liens vers des issues et historique du chat.

Response 200 :

{
  "id": 1, "title": "Sprint planning",
  "sources": [{ "id": 1, "source_type": "youtube", "title": "My video", "url": "...", "status": "done", "duration_seconds": 3600 }],
  "issue_links": [{ "issue_id": 42, "title": "Issue title" }],
  "chats": [{ "role": "user", "content": "Summarize", "created_at": "..." }]
}

DELETE /api/crm/projects/:name/notes/:id

Supprime la note et toutes ses sources/chats. Cascade vers note_sources, note_chats, note_issue_links.

POST /api/crm/projects/:name/notes/:id/sources

Ajoute une source (upload de fichier ou URL).

Content-Type : multipart/form-data OU application/json

Response 201 : { "source_id": 5, "status": "queued" }

Le traitement est asynchrone. Poll GET /notes/:id jusqu'à source.status === "done".

PATCH /api/crm/projects/:name/notes/:id/sources/:sourceId

Renomme une source (édition inline du titre).

Body : { "title": "New name" }
Response 200 : {}
Passe une chaîne vide ou null pour revenir au nom de fichier/URL par défaut.

DELETE /api/crm/projects/:name/notes/:id/sources/:sourceId

Supprime une source et son contenu.

GET /api/crm/projects/:name/notes/:id/sources/:sourceId/progress

Flux SSE de la progression du traitement de la source.

Événements : progress { "status": "processing"|"done"|"error", "message": "..." }

POST /api/crm/projects/:name/notes/:id/chat

Envoie un message au chat de la note. Réponse en flux SSE.

Body :

{
  "message": "Summarize all sources",
  "selectedSourceIds": [1, 3]
}

selectedSourceIds est optionnel — omets-le pour inclure toutes les sources.

Événements SSE :

Stratégie RAG : recherche sqlite-vec sur les embeddings note_source → fallback injection directe de content_text (80 K chars max) quand la recherche vectorielle est indisponible ou sans résultat. Garde anti-hallucination injectée dans le prompt système quand des sources non traitées sont incluses.

Tool use — create_issue : Claude peut créer des issues projet depuis le chat. Multi-tours : le tour 1 streame jusqu'à l'appel d'outil, le backend exécute (issueQueries.nextId + issueQueries.insert), le tour 2 reprend le streaming avec le résultat de l'outil injecté.

Machine à états du statut des sources

queued → processing → done
                    ↘ error

Valeurs du champ status d'une source :

Stratégie de transcript YouTube (Phase 78.3)

  1. npm youtube-transcript : cascade de langues ["en", "en-US", "en-GB"] → fallback n'importe laquelle
  2. API Supadata.ai : GET https://api.supadata.ai/v1/youtube/transcript?url=...&text=true&lang=en → fallback sans le paramètre lang
  3. yt-dlp + Whisper : dernier fallback pour les vidéos sans sous-titres

Priorité : préférer les sous-titres anglais pour éviter des transcripts auto-traduits en arabe ou autre langue.