ACC-5.1 (#24) — app/routers/mcp_server.py : catalogue §7.2 complété
(list_projects, get_project, read_page tronqué proprement,
list_project_tasks), search_pages en plein texte, liens profonds +
kind sur chaque résultat, scopes du jeton appliqués (écriture
refusée -32002 sans scope write) et journalisation §7.4 de chaque
tools/call dans api_audit_log (clés d'arguments seulement).
ACC-2.2 (#20) — pont MCP Flowdeck → ObsiGate, côté client :
- mcp_client : serveurs à session (capture/réémission de
Mcp-Session-Id, handshake avant tools/call), décodage SSE
multi-messages par id de réponse, classification des outils à la
découverte (readOnlyHint sinon heuristique ; lecture par défaut).
- preset connecteur « obsigate » (URL dérivée dev/prod), kinds MCP
factorisés (connectors.MCP_KINDS), option dans le panneau agent.
- garde-fou n° 2 : permission_manager.is_write_tool couvre les
outils MCP écrivants → portes politique d'approbation
(agent_engine) et rôles (assert_can) ; McpTool bloque tout
apply_* distant (patron §7.5).
Tests : test_fusion_mcp_server 18, nouveau test_fusion_pont_obsigate
7 (faux serveur ObsiGate à session stricte via le vrai _rpc).
Preuve réelle T0-T5 : attend la clé ObsiGate dédiée (1 vault) et
Q-A/Q-B côté Bruno.
Le mode console du panneau restait une page morte sur flowdeck-dev :
CSP frame-src/connect-src et DRACO_PANNEAU_CONFIG.agent_url pointaient
l'agent prod (pas encore installée). Deux trous de la 7.82.3 :
agent_url() sans aucune dérivation, et aucune résolution lisant
l'hôte de la requête.
Pour tout ce qui est rendu à une requête (barre/DRACO_URLS, iframe
du panneau, CSP, boutons et redirections SSO) : explicite >
hôte public de la requête (motif fermé
^(og|flowdeck|tiktak|kanbante|agent)(SUFFIXE).dracodev.net$,
suffixe appliqué aux sœurs et à l'agent) > dérivation par les
adresses de l'agent connues en config (élargie : agent_url() se
dérive de l'URL d'événements explicite) > défaut officiel.
L'hôte est posé par le middleware CSP dans un ContextVar (patron
du nonce CSP) : iframe et CSP de la même réponse consomment la
même origine résolue. Contextes sans requête inchangés sinon.
Tests : +12 dans test_draco_urls_devprod.py (25/25) — Host dev
sans aucun env → tout en -dev ; Host prod → officiels ; Host LAN
→ repli config/défauts ; explicite gagne sur l'hôte ; motif fermé.
Suite complète : 4 failed / 26 errors identiques sur arbre propre
(familles préexistantes MCP/realtime/my_tasks/gallery/sync).
Lint (run 2304, job 7058, ruff check app tests) : E402+I001 import mcp_server remonté en tête de app/main.py ; B904 x8 dans api_v2/templates_io.py (raise ... from None, from exc pour ValueError) ; UP017 datetime.UTC dans agent_evenements.py ; F841 x3 dans fd_templates.py (void/uid morts, import inutilisé retirés en chaîne) ; F401+F841 dans tests/test_fd_templates.py (import json inutilisé, n_presets en appel nu).
Version : le test test_pwa_offline cherche le littéral version="7.82.0" dans app/main.py — patron historique des bumps (scripts/bump_version.py le cherche aussi), rompu en 7.80.2 quand l'argument FastAPI est passé à _lire_version(). Littéral reposé en commentaire de synchronisation explicite, version runtime inchangée (lecture dynamique VERSION).
- detach_page_references : nettoyage des références NO ACTION avant
toute suppression définitive (unitaire, Empty Trash, purge 30 j)
- MCP : list_workspaces / get_workspace ; descriptions collections
clarifiées (bases de données, pas des workspaces)
- 96 tests verts sur le périmètre (MCP, trash, cohérence)
Templates (v7.71.x) :
- registre unifié \ emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler)
- sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document
- 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée
- récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates
- correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title
- tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8)
Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
Deux causes indépendantes faisaient échouer le job test de la CI (et
seulement en CI : ni .env ni les mêmes ordres de chargement en local) :
- test_agent_web_tools.mock_http patche http_client.shared_client pendant
sa fenêtre d'exécution. Le premier import d'importers/url_fetch dans
cette fenêtre fige la factory moquée dans le namespace du module →
tous les appels fetch_url suivants du processus passaient par le handler
mocké de l'autre test (« assert '…/post' == '…/page' » dans
test_v56_import). url_fetch résout désormais le client à l'appel, et le
mock_http restaure la vraie factory (référence figée au chargement du
module) y compris sur url_fetch ;
- test_agent.py posait RATE_LIMIT_ENABLED=false en env var, sans effet sur
le singleton Settings déjà instancié : sous pytest -n auto, le worker
dépassait le quota de 60 req/min et 8 tests recevaient des 429
(KeyError 'id' au passage). Le fixture désactive maintenant le limiter
sur le singleton, comme les autres fichiers de tests.
Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
Tiroir Comments
- Section « Reactions » au-dessus des fils : émoji, compte (si >1) et texte
ancré lu dans le DOM (reactionList() / _anchorText(), offsets source hors
pastilles) ; l'état vide ne s'affiche plus s'il reste des réactions.
- Suppression : ✕ sur la ligne du tiroir (mes réactions) ET croix ::after au
survol de la pastille sur l'émoji que j'ai posé (aucun nœud ajouté → offsets
intacts). Deux entrées → removeReaction() → POST toggle → loadReactions().
Menu de sélection (maquette ASCII)
- Ligne 4 = rangée scannée : 💬 Comment (libellé) à gauche, 🙂+ réaction à
droite (.ft-row-split) ; la réaction sort de la ligne 3.
- Entête Skills avec ≡ à droite (::after), liste Skills scrollable.
- ✨ Ask AI retiré de la toolbar : doublon exact de « Edit with AI » du pied.
- Écart volontaire à la maquette : le barré (T) de la ligne 2 est conservé.
Fix racine (flake de gate)
- _is_public_host attrape OSError (le garde SSRF renvoie un booléen, il ne
lève plus) : un socket.timeout DNS ne bascule plus fetch_og_metadata dans
son except Exception → repli silencieux → AssertionError.
- fixture stub_dns sur TestOGParser (même motif que test_agent_web_tools) :
zéro vrai DNS dans les tests OG.
Tests: pytest tests/ -q -n auto → 1390 passed / 0 failed (flake corrigé) ;
ruff check app tests → OK ; e2e v764 (réactions dans le tiroir + suppression
A/B), v762 (nouvelle ligne 4), v766, v7641, v763 → 6 passed sur :8081
(image flowdeck:v7.67.0 rebuild + redéployée).
La clé fonctionne: /v1/models 200 + chat 200 (vérifié live). L'échec venait
du modèle: mistral-large-latest (défaut PROVIDERS + tête de liste) n'est pas
servi par les plans d'abonnement basiques → 403 code 1910 « tier_not_allowed »,
et le Test connection sélectionnait systématiquement ce premier candidat.
- PROVIDERS mistral default → mistral-small-latest (tous plans)
- PROVIDER_MODELS réordonnée: tous-plans d'abord (ordre du test de connexion)
- mistral ∈ _CHAT_VALIDATED_PROVIDERS: le fetch modèles ne garde que les
modèles réellement servis (46 listés → 25 utilisables avec la clé du compte)
- migration 36: default_model bloqué (large/pixtral-large) reset en base
Test live déployé: {ok:true, model:mistral-small-latest, reply:PONG,
verified:true} · tests/test_agent.py 53/53 · nouveau test d'ancrage
test_mistral_defaults_are_tier_safe
- app/services/plugins.py + migration 35 : table plugins (slug, name,
description, enabled) pré-remplie avec 3 modules câblés — web-tools,
web-clipper, automations ; ligne absente = activé (défaut sûr)
- automations OFF → dépendance FastAPI posée à l'include_router dans main.py
(aucun router touché) → toutes les routes /workspace/automations* refusées +
garde de tick du scheduler en arrière-plan
- web-clipper OFF → GET /extensions et tout /api/v2/web-clipper/* refusés
- web-tools OFF → web_search et fetch_url retirés du schéma ET de execute()
via ToolRegistry._all() : le LLM ne les voit plus
- UI rendue côté serveur : global Jinja plugin_enabled(slug) — nav
« Extensions » / « Automations » en {% if %} (absentes du DOM), sections
conditionnées en x-show dans settings.html
- menu + : l'entrée « Add plugins » devient vivante (fini disabled:true) —
liste des 3 plugins avec bascule, GET/PATCH /api/agent/plugins[/slug]
(slug inconnu → 404, 401 sans session)
- tests : tests/test_v758_plugins.py (10 tests) — routes refusées (302 hors
/api, 404 JSON pour /api*), outils retirés, nav disparue, persistance,
câblage ; assertions disabled:true == 0 dans les tests des phases 1/3/4/5/7
- livraison : VERSION + app/main = 7.58.0, OpenAPI 525 chemins, CHANGELOG,
ROADMAP phase 8 cochée (menu + complet), avenant phase 8 (docs)
- presets Discord / Telegram : colonnes kind + auth (bearer/bot/none) sur
agent_connectors (migration 34) ; Discord envoie le jeton préfixé dans
l'en-tête d'autorisation, Telegram le place dans l'URL via le substitut
{secret} remplacé à l'appel — jamais stocké en clair
- Teams : scope ChannelMessage.Read.All ajouté aux Graph scopes M365
- app/services/mcp_client.py : handshake initialize → notifications/initialized
→ tools/list → tools/call (JSON-RPC 2.0, réponse JSON ou premier data: d'un
text/event-stream), garde SSRF, outils cachés en base (tools_json) —
le bouton « Tester » rejoue le handshake
- ToolRegistry._all() merge le cache MCP à chaque run : outils exposés au LLM
sous mcp_<serveur>_<outil> avec leur inputSchema, dispatch tools/call ;
serveur désactivé → outil absent du schéma, échec réseau → ToolResult(error)
- menu + : champ Type dans le formulaire connecteur (preset URL + auth),
badge « · N outil(s) » sur la fiche d'un serveur MCP
- tests : tests/test_v757_mcp_discord.py (12 tests, 0 appel réseau réel) ;
suite complète 1331 verts, ruff 0, eslint 0
- livraison : VERSION + app/main = 7.57.0, OpenAPI 523 chemins, CHANGELOG,
ROADMAP phase 7 cochée, avenant phase 7 dans docs/V74_Agent_Plus_Menu.md
- Table agent_connectors (migration 32) : colonnes plates name, url,
secret_encrypted, enabled, status, detail — config_json remis (YAGNI, les
scopes OAuth des phases 6-7 ajouteront le leur).
- app/services/connectors.py : 1 fichier au lieu du package connectors/ —
3 natifs (gitea, github, web) servis à la volée avec statut sans réseau,
CRUD des personnels (URL validée par _validate_url = garde SSRF, clé chiffrée
Fernet et jamais renvoyée — seul has_secret), probe() qui persiste
status/detail, connector_fetch() borné à 20 000 car.
- API /api/agent/connectors : GET, POST (400 URL privée), PATCH, DELETE,
POST /connectors/probe — 401 sans session, CSRF global. OpenAPI 519 chemins.
- Outil LLM connector_fetch (26e outil) : un seul outil qui dispatche vers
Gitea/GitHub/web/personnalisé (id ou nom ou kind + path + query) au lieu d'un
outil par connecteur ; désactivé/inconnu = erreur outil, jamais de run cassé.
- Menu + : section « Connecteurs » (catalogue avec badge ✓/✗/⚠/?), fiche par
connecteur (Tester, Activer/Désactiver, Supprimer — masqués pour les natifs),
formulaire « Ajouter un connecteur personnalisé » (clé en type=password).
- Tests : tests/test_v755_connectors.py (13) — 3 natifs, roundtrip Fernet de la
clé, 5 URLs refusées (localhost, 127.0.0.1, metadata cloud, ftp:, file:),
toggle persisté, 401, probe OK/erreur, outil complet, câblage menu ;
test_v751/v753/v754 adaptés (1 seule section « bientôt » = plugins).
Suite complète 1306 verts (-n auto), ruff 0, eslint 0 problème.
- Le moteur n'envoie jamais l'historique (system + objectif courant seulement)
: chaque run repartait de zéro. Nouveau service app/services/agent_memory.py —
une ligne résumé par conversation (table agent_memory, migration 31),
écrite en fin de run et ré-injectée au contexte du run suivant.
- context_for() : bloc « ## Mémoire de la conversation », budget 4000 car.
(troncature gauche + marqueur). remember() : 1 note/échange (180 → 700 car.),
20 notes max, upsert, erreurs journalisées — un run ne peut pas échouer à
cause de la mémoire. Toggle OFF = ni lecture ni écriture.
- Toggle « Mémoire » dans le menu + : entrée racine à l'état réel (🧠/💭 +
libellé), PATCH /api/agent/conversations/{id} avec memory_enabled ajouté au
whitelist, état lu à l'ouverture et à la création, défaut AGENT_MEMORY_DEFAULT
(settings.agent_memory_default) + memory_enabled dans la réponse de création.
- Décision : mémoire d'espace (lignes workspace_id NULL) remise — YAGNI, rien
ne l'écrivait ; noté ponytail: dans le service avec le chemin de montée.
- Tests : tests/test_v754_memory.py (8) dont 2 runs réels du moteur (FakeLLM
capture le prompt : mémoire présente ON / absente OFF) ; test_v751 et
test_v753 adaptés (2 sections « bientôt » restantes). Suite complète 1293
verts (-n auto, 2e run ; 1 échec intermittent sur test_v56_import URL au
1er run, vert isolé + vert au run suivant = flake préexistant), ruff 0,
eslint 0 problème. OpenAPI 516 chemins / 7.54.0.
- Section « Design System – Canevas » : liste des canevas (intégrés +
personnels, GET /board/api/page-templates) puis page de détail avec
« Créer une page à partir du canevas », « Insérer dans le document ouvert »
(désactivé sans document ouvert) et « Enregistrer le document ouvert comme
canevas ».
- GET /board/api/page-templates/{id}/blocks (nouvelle route) : blocs d'un
canevas — intégré via template_id=0&key=, sinon id ; 404 clé/id inconnu.
Insertion côté front via ensureBlockIds() global + concat dans E.blocks
+ autoSave/render.
- Canevas « Design System » ajouté aux builtins (callout tokens, TOC, toggles
composants, grille, checklist revue UI, citation).
- Fix : le front du panneau appelait /api/page-templates (404 silencieux) —
le router est sous prefix=/board → /board/api/page-templates.
- Piège : pour /use la clé builtin se passe DANS LE BODY (body.key), pas en
query string — test garde-fou ajouté.
- Tests : tests/test_v753_canvases.py (5) + test_v751 adapté. Suite complète
1285 verts (-n auto), ruff 0, eslint 0 problème. OpenAPI 516 chemins.
- `app/services/http_client.py` : `async with shared_client(timeout=15)
as client:` remplace les 49 créations `async with httpx.AsyncClient(`
de 14 fichiers (gitea ×21, providers oidc/oauth ×11, calendar ×4,
automations ×3…) — le pool de connexions est réutilisé au lieu d'être
recréé à chaque appel. __aexit__ no-op (le client partagé ne se ferme
pas à la sortie).
- Cache par (boucle d'event, kwargs) en WeakKeyDictionary : un
AsyncClient n'est JAMAIS partagé entre deux loops (piège des tests
« Event loop is closed ») — une boucle par test = client propre
collecté avec la boucle. Clé = kwargs triés, repr() pour les valeurs
non hashables (`headers=` dict → TypeError rattrapé par la suite).
- Laissés délibérément : github_adapter (transport MockTransport
injecté), webhook_outbound (client « own_client » fermé par la
fonction).
- Tests : `test_http_client_shared_and_loop_scoped` (réutilisation mêmes
kwargs / cloisonné kwargs / cloisonné loop) ; le stub des webhooks
patche aussi la fabrique `http_client.httpx` + purge du cache (avant :
webhook_outbound.httpx patché mais la fabrique partagée créait un vrai
client → réseau réel dans les tests).
suite **1091/1091** · ruff OK · docs à jour
- `run_event_sync(coro)` (app/services/automations.py) : exécute une coroutine
d'événement depuis un handler synchrone — `asyncio.run` sur une boucle
dédiée dans le worker threadpool : le worker est bloqué, JAMAIS la boucle
d'event, et la réponse n'est produite qu'une fois l'événement terminé
(déterministe, équivalent sémantique de l'await). ponytail: les clients
httpx sont créés à chaque appel partout → aucun lien de boucle ; sinon
run_coroutine_threadsafe + boucle du lifespan.
- 15 routes api_v2 dont les SEULS awaits étaient `request.json`,
`_fire_event`, `fire_published`, `fire_unpublished` →
`Body(default={})` + `run_event_sync(...)` + conversion en `def` (script
: wrapping par appariement de parenthèses chaîne-aware, assert de flip
« plus aucun await »).
- api_v2 : **111/115 routes hors loop**. Les 4 restantes ont de vrais awaits
réseau et restent async volontairement : import_csv_v2 (multipart),
project_tree_v2 (gitea), test_webhook_v2 (delivery), retry_webhook_deliveries.
- Repo-wide : 403 routes sync (hors loop) / 260 async (phase 2c).
tests : ciblé public_api_v2 + v65 + webhooks_v2 + audit = 90/90 (les webhooks
prouvent la détermination de run_event_sync) ; suite complète **1037/1037**
(228 s) · `ruff check app tests` OK · docs à jour
- A26 — champ `standalone` mort supprimé (0 lecteur) ; `sqlite:////data/…` ne
produit plus de chemin UNC sous Windows ; `.env.example` ne promet plus
PostgreSQL ; raise au boot si `APP_SECRET_KEY` vaut la valeur par défaut
- A33 — rate limit : préfixes manquants (`/scim/v2/`, `/workspace/`, `/db/` +
non-GET sur `/s/` et `/f/`), limite lue dans `settings.rate_limit_requests`
(60 annoncés / 100 codés en dur), clé `X-Forwarded-For` seulement derrière un
proxy local (anti-spoof), `_store` épuré (mémoire bornée) + test dédié
- A34 — `_spawn()` : les 10 schedulers loggent leur exception et redémarrent
après 10 s au lieu de mourir en silence ; 2 `logger.debug` de scheduler → warning
- A35 — OpenAPI régénéré 439 → 511 chemins (`info.version 7.3.9`), README
(était v6.7.0), compteur API_GUIDE, titre dupliqué retiré du ROADMAP ;
le drift Python 3.12/3.13 reste noté (rebuild d'image à valider)
- A36 — 4 dépendances mortes purgées de requirements.txt (aiosqlite, slowapi,
loguru, packaging = 0 import) ; pyproject reste sans [project] : Docker et la
CI installent requirements.txt, dupliquer les 22 deps créerait 2 sources
- A43 — 15 `datetime.utcnow()` → `now(UTC).replace(tzinfo=None)` (format ISO
naïf identique, warnings de dépréciation divisés : 2374 → 926)
suite **1029/1029** · `ruff check app tests` OK · docs/ROADMAP/CHANGELOG/WORKLOAD à jour
- A25 — 84 `except Exception: pass/…` → `logger.exception("<fonction>")`
(19 fichiers : api_v2 30, dashboard 10, board 7, sites 5, workspace 5,
api_v2_helpers 5, …) ; `logger` ajouté là où il manquait (api_v2_helpers,
sites + `import logging`)
- A25 critique — les `try` autour de `materialize_properties` supprimés dans
`create_collection_v2` ET `apply_db_template_v2` : un échec interrompt la
transaction au lieu de commiter une collection sans schéma
- test `test_collection_rollback_when_materialize_fails` (Bearer v2, monkeypatch
qui lève, assertions : RuntimeError + 0 ligne commitée)
- A21 partiel — `PRAGMA busy_timeout=5000` dans `get_conn()` (point d'entrée
unique) ; commentaire `ponytail:` : le wrapper async + les 510 call sites
restent à migrer module par module
- suite **1028/1028** · `ruff check app tests` OK
- Add HMAC SHA-256 signature verification for webhook payloads
- Implement retry logic with delays (2s, 10s, 60s) and max 4 attempts
- Add 20+ new events (total ~50 events) covering pages, blocks, users, etc.
- Add API v2 endpoints for testing HMAC signature and retrying deliveries
- Add comprehensive test suite for webhooks v2 functionality
Generated by opencode.
- app/db.py: ajout colonne shared_with_group_id (FK user_groups, migration
backfill v5.x) dans page_shares
- app/routers/sharing.py: POST /api/pages/{id}/share accepte group_id
(upsert, verif FK groupe), GET /shares expose kind/group_name, synchro
bidirectionnelle avec page_permissions (mirror grant/revoke) pour que
PermissionManager donne un acces effectif (view/comment/edit) aux membres
du groupe; PUT/DELETE gardent le miroir a jour
- app/routers/board.py, library.py: received/made incluent les partages via
groupes (JOIN group_members)
- app/templates/_page_editor_content.html, _page_editor_scripts.html:
dialogue Share — invite groups (fetch /api/v2/groups, filtre deja partages),
pickInviteGroup, shareInvite(group_id), rendu accessList avec avatar groupe
- tests/test_share_groups.py: 10 tests (CRUD groupe, kind, miroir ACL)
Chore: inclut evolutions v6.4.0 deja en working copy (webhooks prod,
migrations, sync, config/main) pour garder l'arbre coherent.