- Inventaire exhaustif des 13 noms `function NAME` définis 2+ fois
(templates + static/js) avec scan de profondeur de brace (strings,
comments, backticks gérés) : 12 sont déjà scopés dans des IIFEs
depuis A27 (escHtml/flush/emit/setMeta/initials/up/esc/show/close…) —
aucun conflit de page possible.
- Seul doublon GLOBALE = openCardDetail (corps byte-identiques ×2 dans
board_fragment + detailed_board, fragments de vues mutuellement
exclusifs) → dédupliquée vers static/js/app.js, 2 copies supprimées ;
les onclick/@click des deux fragments appellent la même définition
(owner/repo globaux fournis par board.js au moment du clic).
- test_no_duplicate_global_functions : garde-fou 0-doublon entre
templates et static/js (scanner naïf, plafond ponytail commenté).
Reste A38 : méthodes jumelles library/local_workspace (~9-21 noms
communs, corps divergents) → fusion workspace-tree.js reportée
(réconciliation sans E2E, même logique que A39/A20).
suite **1093/1093** · ruff OK · node --check vert · docs à jour
A43 (les 4 items sont clos) :
- `__CSRF_PLACEHOLDER__` supprimé : ContextVar CSRF_TOKEN posée par
CSRFMiddleware avant call_next (même mécanique que le nonce CSP),
global `{{ csrf_token() }}` dans templating, base.html rend
`{"X-CSRF-Token":{{ csrf_token()|tojson }}}` — vide si cookie absent,
`htmx:configRequest` re-lit le cookie à chaque appel → plus jamais de
jeton factice servi.
- Palette : `fetch('/api/search…')` SANS header (GET ∈ SAFE_METHODS →
CSRF inapplicable) — le JSON.parse du body-attr par frappe disparaît.
- utcnow déprécié = 0 dans app/**.py ; health loggé (A25) + booléens db/
gitea = raison ; probe réseau = voulu (test de connectivité).
A38 phase 1 (CSRF unifié) :
- `window.getCsrf()` unique dans le <head> de base.html.
- 76 lectures brutes du cookie → getCsrf() dans 13 fichiers (47 formes
`(…||[])[1]||''`, 25 déclarations `const X = match(…)` avec conversion
de leurs usages `X?X[1]:''` → `X` (noms collectés PAR FICHIER, jamais
de règle globale), 4 formes espacées).
- Définitions dupliquées supprimées : card_detail.html (multi-lignes),
database_table.js (1 ligne dans IIFE → chute sur le global).
- Les 3 variantes de base.html (IIFE getCsrf + 2 getCsrfToken) →
`return getCsrf()`.
- welcome.html garde sa lecture locale (page autonome sans base) ;
reste exactement 1 raw dans base.html (la déf head) + 2 dans welcome.
Test : test_csrf_server_rendered_no_placeholder (pas de placeholder +
token du hx-headers == cookie csrf_token).
suite **1092/1092** · ruff OK · node --check vert · docs à jour
- `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
- Vendorisation : chart.js 4.5.1 + leaflet 1.9 (leaflet.js, leaflet.css,
5 images marker/layer) vers static/js/vendor/ (déjà ignoré par eslint) ;
les 3 URL CDN des vues chart/map (collections.py) pointent en local →
la CSP n'a plus AUCUN hôte tiers dans script-src ni style-src.
- connect-src fermé : `'self' ws://{host} wss://{host}` — Host de la
requête (uvicorn rejette déjà les Host invalides) + filtrage des
caractères hors base URL. Le `https:` universel (canal d'exfil) et les
ws:/wss: tout-hôtes disparaissent. Grep négatif : 0 fetch cross-origin
côté front.
- Google Fonts : entrées CSP mortes (0 référence dans le code) retirées
de style-src/font-src.
- img-src https: CONSERVÉ volontairement (unfurls YouTube/Vimeo… + tuiles
OSM inénumérables) — ponytail: commenté dans security.py.
Tests : test_csp_no_cdn_and_vendor (CSP sans CDN/Google, connect-src
exact 'self' ws://testserver wss://testserver, 4 assets vendor 200,
source collections.py sans CDN) + test_view_chart_renders mis à jour
(chemin vendor). Suite complète 1090/1090 (1089 + 1).
Reste A20 : unsafe-eval (Alpine x-data + htmx hx-on/hx-vars = eval)
→ build @alpinejs/csp + couverture E2E des vues d'abord (même logique
que la décision A39).
suite **1090/1090** · ruff OK · docs à jour
- `_database_table_scripts.html` → `static/js/database_table.js` (1 314 L).
Le Jinja du bloc était confiné à la construction de l'objet de config
(4 clés + `{% if collection_data %}`) → config JSON `#db-config`
null-vs-objet : `new DBInstance(container, PAGE_COLLECTION_ID, DB_CONFIG)`
remplace les 2 branches Jinja (le `else` était déjà un literal null).
- Loader DB_CONFIG : JSON.parse du bloc, `null` si absent (parité stricte
avec le else d'origine) ; acrlade try corrigée par node --check avant
commit.
- 2 tests adaptés (lisaient le template source → static/js/database_table.js)
; `FlowDeckDB` / `db-board` / `db-cal-grid` / `db-gallery` plus dans le
HTML → asserts sur le JS extrait.
BILAN A27 : 11 874 L extraites en 4 phases (4 243 + 2 516 + 3 801 + 1 314),
inline 13 904 → 2 022 L (-85 %), 22 fichiers static/js/*.js, node --check
vert partout, eslint 0 erreur / 285 warnings. Reste : base 1 523 L
structurel ({% block %}/{% for %} — inline par nature), ~500 L de petits
blocs hors cibles, nettoyage des 285 warnings.
suite **1089/1089** · ruff OK · docs à jour
- `_page_editor_scripts.html` : le gros bloc interpolé (2 516 L) part vers
`static/js/page_editor_scripts.js` — recette « config JSON » : les 8
interpolations Jinja lisent `PD = JSON.parse(#page-data)`, bloc JSON qui
EXISTAIT DÉJÀ juste avant le script (même ordre d'exécution), garde
`__fdEditorScriptsLoaded` préservée, node --check vert.
- Route `view_page_root` : page_data enrichi de updated_at, created_at,
user_id, is_shared (dérivé HOISTÉ : une seule expression sert le ctx ET le
JSON) et clip_icon (macro fd_icon rendue côté serveur). workspace_key reste
vide comme avant (jamais défini dans ce ctx → parité stricte).
8 tests adaptés à l'extraction (ils lisaient le template SOURCE) :
- test_ai_writing ×2 (+ helper _read_js), test_pwa_offline,
test_v511 front_end_wired, test_v55 ×3 → lisent le JS extrait
- test_page_editor_renders_page_is_shared → parsing du JSON #page-data
(`is_shared is True`) — la valeur sert toujours à la page
Cumul A27 : 6 759 L extraites (13 904 → 7 145 inline). Reste : local_workspace
2 031, base 1 523 (structurel {% for %}/{% block %}), database_table 1 323,
settings 1 093, realtime 531, board 146 ≈ 6 653 L + 120 warnings eslint.
suite **1089/1089** · ruff OK · docs à jour
Les 6 dernières routes d'A32 (api.py, gitea) avec stub de transport — zéro
réseau réel :
- _stub_gitea() : stubs manuels sur gitea_client.gitea (create_issue,
update_issue, update_issue_labels, get_issue, get_issue_comments) avec
ÉTAT MUTABLE PARTAGÉ — le handler PATCH re-fetch l'issue via get_issue,
un canevas figé aurait masqué la mise à jour.
- POST /issues : carte INSÉRÉE sur le board (board seedé par endpoint) ;
PATCH : colonne recalculée sans perdre la carte.
- GET /issues JSON + HTML : ?format=html requis (le segment /html ne fixe pas
le paramètre, le handler le lit dans la query) ; stub qui lève → 404.
- POST /checklists + POST /checklist-items : lignes vérifiées en base,
404 sans board ; cleanup (items → checklists).
BUG PROD corrigé (trouvé par le smoke HTML) : card_detail.html utilisait la
macro fd_icon SANS l'importer → UndefinedError → 500 systématique sur
GET /api/issues/...?format=html (seul rendu du template dans le code).
Fix : {% from '_icons.html' import fd_icon %}.
A32 COMPLET : plus aucun router « 0 test » (webhooks, notes, sidebar_config,
github_routes, library, api, dashboard, api_v2 tous couverts).
test_smoke_uncovered.py : 52 tests. suite **1089/1089** · ruff OK · docs à jour
+5 routes dashboard (fichier test_smoke_uncovered.py à 49 tests) :
- Members POST/PUT/DELETE : invitation de soi-même dans un workspace dédié
(_own_workspace), rôle admin relu en base, membre supprimé (COUNT=0).
Quirk documenté : les retours tuple des routes (`{"error": ...}, 400`)
sont sérialisés FastAPI en tableau + 200 → assert sur `[0]["error"]`.
- upload-folder : validations SEULES (structure absente → 400 « No
structure provided », JSON cassé → 400 « Invalid structure JSON ») —
zéro fichier écrit, workspace dédié nettoyé.
- convert-to-database : collection + propriété title + vue table + page en
content_format='collection' VÉRIFIÉS en base, 404 page inconnue,
cleanup dans l'ordre FK (pages avant collections — IntegrityError corrigée).
Recoupement final : scan des 44 routes strictement à 0 ref de dashboard.py →
TOUTES exercées. Les 19 résidus du scan sont des faux positifs (paths en
f-string dans les tests : /api/workspace/1/…, f"/api/pages/{id}/…", …)
rapprochés manuellement un par un.
Reste A32 : les 6 routes Gitea d'api.py (issues ×4, créations checklists)
→ stub de transport httpx (effort S).
suite **1086/1086** · `ruff check app tests` OK · docs à jour
Cumul dashboard : 43 → 56 des 63 routes. 4 nouveaux tests (fichier à 46) :
- GET /gitea-workspace : page HTML (200 ou redirection propre)
- workspace/projects GET+POST : shape {builtin, gitea, github} avec
github == [] ; projet créé RETROUVÉ dans builtin ; quirk « error » sans nom
; nettoyage (DELETE page)
- Cycle items local-workspace (5 routes) : POST création (titre relu),
PUT rename (relu en base), PUT move, DELETE soft-delete (deleted_at relu),
POST restore (deleted_at NULL relu) — nettoyage finally
- Cycle tags d'item (5 routes) : POST (urgenta32 lowercasé), tags de l'item,
liste workspace, search (shape), suppression vérifiée. Utilisateur DÉDIÉ +
workspace créé dans le test (le endpoint /api/local-workspace/tags exige un
workspace actif : fallback « premier workspace du user » — on n'attache pas
ce workspace à l'utilisateur fixture partagé), tout est nettoyé.
Reste A32 : dashboard 7 routes (members invite/role/unsubscribe,
upload-folder, convert-to-database) + 6 routes Gitea d'api.py (stub httpx).
suite **1083/1083** · `ruff check app tests` OK · docs à jour
Cumul dashboard : 36 → 43 des 63 routes. 4 nouveaux tests (fichier à 42),
centrés sur les garde-fous A16 :
- GET /api/files/{ws}/{path} : traversal encodé %2e%2e%2f → 403
« Path traversal denied » (décodé par Starlette puis bloqué par resolve) ;
inexistant → 404 ; vrai fichier écrit dans le data_dir de test →
200 + octets exacts, nettoyé en finally
- GET /api/pages/{id}/download : page markdown → 404 « No downloadable file »
(pas de 500) ; page « file » avec chemin ../ sortant de la racine →
jamais 200 ; file-content → 404/415 sans fuite
- GET /api/local-workspace/page-content/{id} : contenu + format relus,
404 sur id inconnu
- GET /api/avatar/{id} : 302 + Location avec follow_redirects=False
(AUCUNE requête réelle vers l'URL externe), 404 sans avatar
- GET/POST /api/collections/{id}/table-data|pages : 404 inconnu, shape,
ligne créée retrouvée dans table-data, nettoyage finally
Reste A32 : dashboard 20 routes (upload/local-workspace items/members/
projects/HTML gitea) + 6 routes Gitea d'api.py (stub transport httpx).
suite **1079/1079** · `ruff check app tests` OK · docs à jour
Cumul dashboard : 27 → 36 des 63 routes. 6 nouveaux tests (fichier à 38) :
- /accounts + /accounts/settings : 200 HTML et « password_hash » ABSENT du
rendu (whitelist A2 vérifiée côté page)
- PUT /api/user/profile : persistance relue en base, restauration finally
- PUT /api/user/password : 403 « current password is incorrect » (A3 — la
session seule ne change pas le mdp) + quirk assumé documenté : la longueur
est validée AVANT l'auth et répond 200 + message
- POST /api/user/token : format fd_ + 64 hex ; ligne user_tokens nettoyée
- DELETE /api/user/forge/{provider} : {"status": "ok"}
- PUT /api/settings/account : full_name/email persistés + 400 sur mdp court,
restauration finally
- POST /api/workspaces/1/select : Set-Cookie flowdeck_workspace vérifié ;
GET /api/local-workspace/breadcrumb : shape liste
Reste A32 : dashboard 27 routes (fichiers/avatars/local-workspace/collections)
+ 6 routes Gitea d'api.py (stub transport httpx).
suite **1075/1075** · `ruff check app tests` OK · docs à jour
Scan strict dashboard.py : 44 routes à 0 référence stricte sur 63. 10
couvertes cette passe (test_smoke_uncovered.py → 32 tests) :
- Tags CRUD complet : POST (nom lowercasé `SmokeTag` → `smoketag`), GET all
(présent), PUT couleur (relue), DELETE (absente de la liste ensuite)
- Vie d'une page : GET /api/pages/{id}/content (contenu seedé relu) →
PUT rename (ok + **400 titre vide** + titre relu en base) →
POST trash (parent_section='Trash' + deleted_at RELUS en base) ;
nettoyage en finally
- GET /api/sidebar/workspace-tree : 200 HTML, fragment « No pages yet »
(pas de cookie workspace)
- POST /api/settings/avatar-color : couleur relue SUR L'UTILISATEUR DE LA
SESSION (pas LIMIT 1), avatar_color/avatar_url d'origine restaurés
- GET /api/workspace/1/members : shape {"members": [...]}
Helper _seed_page : les colonnes par défaut sont surchargeables (content=)
pour les seeds à contenu.
Reste A32 : dashboard 34 routes à 0 ref (fichiers/avatars/imports…) +
6 routes Gitea d'api.py (stub transport httpx).
suite **1069/1069** · `ruff check app tests` OK · docs à jour
Scan strict des 115 routes api_v2.py contre tous les tests (chaîne de chemin
littérale) → 5 routes sans AUCUNE référence, toutes couvertes maintenant :
- POST /properties/evaluate-formula : 200 + shape, 400 sans expression.
Le moteur renvoie « 1 + 2 » tel quel aujourd'hui → le smoke valide le câble
(bearer, Body param, parse), pas le moteur (réalm de ses propres tests).
- POST /properties/compute-rollup : 400 « collection_id required »,
401 sans bearer.
- GET /admin/audit-logs : portail admin VÉRIFIÉ — l'attendu est calculé
depuis /users/me (le tout premier utilisateur d'un worker est admin :
état non contrôlable depuis un test isolé), + token scope admin → 200 + logs.
- GET /webhooks/events : catalogue non vide + wildcards * / page.*.
- POST /webhooks/verify-signature : valid=True avec sign_payload() (le même
helper que le serveur), False avec signature bidon.
test_smoke_uncovered.py : 27 tests. Reste A32 : dashboard 17/63 + 6 routes
gitea d'api.py (stub transport).
suite **1064/1064** · `ruff check app tests` OK · docs à jour
+6 smokes dans test_smoke_uncovered.py (22 tests au total dans le fichier) :
- board-config GET/POST : défauts 5 colonnes sans board, création puis
relecture du roundtrip (seed via l'endpoint lui-même, pas de SQL brut)
- col-mapping POST/DELETE : 404 sans board, upsert label vérifié, suppression
- card POST : 404 sans board, ok avec
- collaborators GET : gitea.get_collaborators STUBBÉ (zéro accès réseau réel)
- frontend-error(s) : capture, JSON invalide → ignored, DÉDUP d'une erreur
répétée (count=2), lecture qui purge (cleared=true puis 0)
- checklist mutations : PATCH item (checked/content relus EN BASE), DELETE
item, DELETE checklist (COUNT=0) — seed + cleanup en finally
Reste api.py : 6 routes Gitea (issues ×4 + créations checklists owner/repo) →
stub de transport httpx (phase suivante). Reste global : dashboard 17/63,
api_v2 50/115.
suite **1059/1059** (236 s) · `ruff check app tests` OK · docs à jour
- tests/test_smoke_uncovered.py : +6 tests pour library.py (1/10 → 8 routes
couvertes) : les 5 listes en boucle (recents/favorites/published/private/
workspace → 200 + items), /private avec page seedée retrouvée, /children/{id}
avec parent/enfant seedés, /repository vide ET clé (string, aucun réseau),
non-régression 404 sur les routes supprimées.
- DÉCOUVERTE (les smokes l'ont prouvé) : `/api/library/local-workspace-children`
renvoyait un 500 systématique (test vert → ASGI double response.start) et
`/api/library/local-workspace` 500 dès qu'un workspace existe — les deux
lisaient `local_workspace_items`, table AUCUNEMENT créée dans le codebase
(0 CREATE TABLE) et sans 1 seule référence front. Supprimés avec
`library._format_size` devenu mort (la copie de dashboard.py est inchangée).
`local_workspace_items` : 0 occurrence restante dans app/.
- helper `_seed_page` minimal (workspace NOT NULL inclus) + nettoyage en
finally (pas de pollution des autres tests).
suite **1053/1053** (229 s) · `ruff check app tests` OK · docs à jour
restent phase 2b : api.py 3/23, dashboard.py 17/63, api_v2.py 50/115
tests/test_smoke_uncovered.py — un smoke par route des 4 routers qui n'avaient
AUCUN test :
- webhooks.py 3/3 : réception sans secret → {"status":"ok"} ; HMAC faux → 401
(secret piloté par monkeypatch, déterministe quel que soit le .env) ;
register sans secret → 400 AVANT tout appel réseau ; status avec
gitea.list_webhooks stubbé → {"registered": False} (zéro réseau réel)
- notes.py 2/2 : GET HTML + roundtrip POST→GET (upsert persisté en base,
échappement HTML vérifié : <b> et non <b>)
- sidebar_config.py 2/2 : GET défauts ; PUT persisté puis RELU depuis
users.sidebar_config ; 400 sans config ; remise en état en fin de test
- github_routes.py 2/2 : status {"linked": False} ; disconnect {"status": "ok"}
Reste (A32 phase 2) : quasi nuls — library 1/10, api 3/23, dashboard 17/63,
api_v2 50/115 → même recette, fixture client existante.
suite **1047/1047** · `ruff check app tests` OK · docs à jour
- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
`script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
(chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
: htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
: vues chart/map déjà BLOQUÉES par la CSP depuis toujours
(commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
JS (A27), resserrer img-src/connect-src
test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)
suite **1037/1037** · `ruff check app tests` OK · docs à jour
- `_apply_one()` : BEGIN explicite → `fn(conn)` → marque `schema_version` →
commit ; rollback complet à l'échec. Avant le DDL sortait en autocommit
(isolation_level legacy) : un échec au milieu laissait un schéma partiel
commité SANS ligne de version, et la reprise rejouait un DDL déjà appliqué.
Si une transaction englobante subsiste (init_db commit juste avant), on la
vide d'abord plutôt que de l'englober.
- Helper unique `columns(conn, table)` (valide l'identifiant, ValueError sinon)
: 25 copies de `{r[1] for r in conn.execute("PRAGMA table_info(...)")}`
éliminées dans migrations.py (21 littéraux + 3 f-string + 1 variante row).
`table_exists`/`column_exists` préconisés par l'audit NON livrés : aucune
migration n'interroge sqlite_master, un contrôle unitaire se lit dans le set.
- Smoke : DB fraîche → 28 migrations → version 29, ré-apply idempotent.
tests : test_migration_transaction_rolls_back (DDL partiel annulé + zéro marque
de version), test_columns_helper_validates_table_name
suite **1036/1036** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.6.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à 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
- `_current_user_id` : 401 sans session (24 sites) au lieu de retomber sur
`SELECT id FROM users WHERE login='admin'`
- `_current_admin` : suppression du même fallback — `PATCH /api/agent/providers`
et `POST /api/agent/providers/test` (donc `LLMClient.ping(api_base=…)`)
exigent une session admin : 401 sans session, 403 non-admin
- `_check_api_base()` sur les 2 routes : scheme http(s), pas d'identifiants
dans l'URL (400) ; hôtes privés maintenus — Ollama `localhost:11434` est le
provider par défaut du produit (commentaire `ponytail:` pour la fermeture)
- +1 test de non-régression → suite **1027/1027**, `ruff check app tests` OK
- Scan de tous les appels `fetch` non-GET du front par préfixe : 12 préfixes
n'ont AUCUN call site sans `X-CSRF-Token` → sortis de `EXCLUDED_PATHS`
(`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`,
`/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`,
`/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`)
- `welcome.html` : les 2 POST `/api/onboarding/*` reçoivent le header
(`adminFetch` prouve que `/api/admin` était déjà couvert)
- reste A19 (front à équiper) : `/api/workspace` (+`/api/workspaces`),
`/api/local-workspace`, `/api/settings`, `/api/gitea`, `/api/agent` — 49 fetch
- tests : helper `anon_csrf()` (anonyme + CSRF valide) pour isoler les 401 de
route du 403 middleware — 4 tests d'anonymat ajustés
- suite **1026/1026** · `ruff check app tests` OK
- A11 : `GET /api/settings/avatar/{filename:path}` → `resolve()` + `relative_to()` (motif de `serve_uploaded_file`), 403 hors de `/data/avatars`
- A18 : `GET /workspace/public/{id}` → 404 HTML explicite pour `permission_type` restricted/private, `html.escape` sur le nom, l'icône et les titres de lignes (le f-string HTML ne passe pas par Jinja2)
- `tests/test_audit_p0_fixes.py` : 3 tests de non-régression (traversal, échappement, hidden restricted)
- ROADMAP A11/A18 cochés · CHANGELOG/WORKLOAD/VERSION → 7.3.2 · suite **1019/1019** · `ruff check app tests` OK
- A3 : helper `_require_user_id()` (401 sans session) sur profile/password/token/forge ; `PUT /api/user/password` exige `current_password` vérifié ; `/api/user` sorti de la liste CSRF exemptée
- A4 : `POST /api/v1/token` → 401 sans session, chemin legacy `user_id=0` supprimé
- A5 : CRUD membres d'espace (POST/PUT/DELETE) : session + rôle admin de l'espace (ou admin global), placeholder user créé en `is_admin=0`, GET membres sans session → 401
- A6 : `_require_view` → 404 et `_require_edit` → 401 quand il n'y a pas de session (fin du legacy single-user sur les collections)
- A7 : création ET lecture de page → 401 sans session (PermissionManager conservé) ; `/board/api/pages` sorti de `EXCLUDED_PATHS` ; header CSRF manquant ajouté sur setItemIcon (local_workspace)
- A8 : seed admin sans mot de passe codé en dur — aléatoire au premier boot loggé une fois, ou `FLOWDECK_ADMIN_PASSWORD` ; re-seed seulement si absent
- tests : client connecté par défaut via `_TestSessionAuth` (session + CSRF injectés à la volée, jamais dans le cookie jar → plus de CookieConflict), helper `anon()` sur les 40 tests d'anonymat ; 1016/1016 verts, `ruff check app tests` OK
Le rebind (`app.config.settings = Settings()`) laissait tous les modules déjà
importés (sso_provisioning, trash, …) sur un objet périmé : le test
test_v67_sso::test_env_config_fallback_when_table_empty échouait dès qu'il
tournait après test_v54 dans le même worker (-n auto). Mutation sur place
comme le préconise conftest.py.
- 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.
Un api_base stocke (ex. https://api.mistral.ai/v2) ecrasait l'URL par defaut corrigee et faisait echouer le test de connexion. Desormais: normalisation a l'ecriture (une base egale au defaut n'est pas stockee), possibilite de vider le champ (api_base='' vs None), et migration 14 qui efface les bases obsoletes/redondantes dans user_llm_keys et llm_config.
Tests de non-regression ajoutes.
Cohere -> /compatibility/v1, Google Gemini -> /v1beta/openai, Perplexity -> host racine, Chutes -> llm.chutes.ai/v1, SenseNova -> compatible-mode/v1. Mise a jour des modeles par defaut (xAI grok-4.6, Fireworks deepseek-v4-pro-0813, Cohere command-a-plus).
Retrait de LTX Studio (aucune API chat-completions) et MemTensor/MemOS (API memoire non OpenAI-compatible, auth Token + /chat). Google utilise desormais l'auth Bearer pour lister les modeles. Test de non-regression ajoute.
Template-created pages (weekly report, project doc, meeting notes, to-do
list, ...) were persisted without block ids. The editor assigned ids
client-side, but the realtime room loaded the raw id-less content and sent
it back on 'sync'; applySync then merged id-less server blocks with local
blocks, producing data-bid='undefined' collisions and duplicated/shuffled
lines as soon as the user edited. Editing an empty page was unaffected
because the server state was empty.
Fixes:
- board.use_page_template: materialize unique block ids (recursively) when
instantiating built-in or user templates.
- realtime_server: unique block_id() (uuid) + ensure_block_ids() on room
load and on insert ops.
- editor: recursive ensureBlockIds() in init; gtTok() now restores
[[fddate:...]] tokens (date chips survived as labels before).
- realtime client: applySync() normalizes ids, no longer appends unknown
local blocks (duplication), and keeps local content when server is empty.
Tests: pytest (templates + realtime) and Playwright e2e covering to-do
list, weekly report date chip, Enter ordering and legacy id-less repair.
Pages created via the page-template picker (weekly report, project doc,
meeting notes, ...) previously got workspace_id=NULL and workspace=login,
so they never appeared in the active local/Gitea workspace tree — they
looked like they 'didn't save' even though the row existed.
Frontend: _useTemplate now sends the active workspace context (workspace_id
or workspace key), mirroring _createPlainPage.
Backend: use_page_template resolves/validates the workspace and persists
both workspace and workspace_id on the new page.