v7.60.0
140
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f132885b08 |
feat: bouton + à gauche du handle de bloc — insère un bloc dessous (v7.60.0)
- nouveau bouton .block-insert-btn émis AVANT le handle (6 points) dans le wrapper de chaque bloc de premier niveau : clic → E.addAfter(idx) → addAt(idx+1) → bloc inséré sous le courant et focusé (mécanisme déjà en place, réutilisé tel quel) - position : left:-64px (gouttière .blocks-container = 64px), -56px dans le breakpoint où la gouttière passe à 32px ; apparaît au survol/focus comme le handle ; masqué avec lui sur écran étroit (2 breakpoints) - aria-label + title « Add block below » - tests : tests/test_v760_block_insert_btn.py (2 tests : ordre dans le wrapper, câblage E.addAfter, positions/masquages CSS) - livraison : VERSION + app/main = 7.60.0, OpenAPI 525 chemins, CHANGELOG |
||
|
|
a79bdc1641 |
fix: « Turn into » du menu contextuel ne fonctionnait pas (v7.59.3)
- cause : la garde de turnInto() faisait _BLOCK_TURN_TYPES.indexOf(type) sur
un tableau d'objets {id,name} → toujours -1 → retour immédiat après
l'ouverture du sous-menu, aucun changement de format
- correction : validation par id (_BLOCK_TURN_TYPES.some(t=>t.id===type))
- même fonction : convertir un texte en To-do / Toggle vidait le contenu
(exclusion par erreur) ; seuls les types sans champ texte (image, embed,
meeting, divider, table_of_contents, database, button, columns) le vident
- tests : tests/test_v7593_turn_into.py (4 tests, dont 1 comportemental node
qui exécute la vraie turnInto() extraite de la source)
- livraison : VERSION + app/main = 7.59.3, OpenAPI 525 chemins, CHANGELOG
|
||
|
|
a67bd252aa |
fix: aide (bouton ✕), Insert below qui perdait le format, listes numérotées invisibles (v7.59.2)
- aide : bouton ✕ en haut à droite du panneau (classe partagée settings-close, 44 px mobile) + helpInit.close() (retour en arrière, sinon /workspaces) ; .settings-panel en position:relative pour ancrer hamburger + ✕ au panneau - éditeur « Insert below » (/Write with AI) : insère le markdown source conservé à la génération (AIC._md) au lieu du texte extrait du HTML rendu (_resultText() reste seulement en repli) → titres/listes/gras conservés - listes numérotées : aucun numéro n'était affiché (CSS = retrait seul) ; le rang des blocs numbered_list contigus est calculé au rendu (data-num) et rendu via content: attr(data-num) — /numbered list et l'insertion agent affichent 1, 2, 3 - md2b : cases à cocher - [ ] / - [x] testées avant la branche des puces (aligné sur paste2b) + séparateur 1) accepté (CommonMark) - menu slash : requête tapée dans le bloc (/numbered list) répercutée dans le filtre du menu quand le focus n'a pas rejoint l'entrée - tests : tests/test_v7592_ui_fixes.py (5 tests, dont 1 comportemental node qui exécute le vrai md2b) ; ruff I001 préexistant sur test_agent.py - livraison : VERSION + app/main = 7.59.2, OpenAPI 525 chemins, CHANGELOG |
||
|
|
39d34ac866 |
fix(llm): Mistral 403 tier_not_allowed — défauts tous-plans + sonde chat + migration 36 (v7.59.1)
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
|
||
|
|
16f73fe39e |
feat: Add plugins — catalogue on/off à effet réel (v7.58.0, phase 8/8)
- 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)
|
||
|
|
f8acb906e5 |
feat: connecteurs Discord/Telegram/MCP + outils MCP dynamiques au registre (v7.57.0)
- 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
|
||
|
|
1d7750bda0 |
feat: connecteurs Google + Microsoft 365 (OAuth2 PKCE) — phase 6/8 (v7.56.0)
- app/services/oauth_connectors.py : flow OAuth2 complet PKCE (S256) pour
2 fournisseurs décrits par 1 dict — Google (Drive/Gmail/Calendar en lecture
seule) et Microsoft 365 (Graph Files.Read / Mail.Read / Calendars.Read) ;
begin() = URL d'autorisation + state + code_verifier, complete() = échange du
code, access_token() = refresh automatique (60 s de marge, refresh_token
conservé si absent de la réponse), api_get() = path absolu refusé + validation
SSRF + borne 20 000 car.
- Tokens chiffrés Fernet en réutilisant calendar_sync._encrypt_tokens (zéro
dépendance) dans la table connector_tokens (migration 33, PK (kind, user_id)).
- 4 routes /api/agent/connectors/oauth/{kind}/… : status, authorize (cookies
d'état HttpOnly 10 min, retour same-origin validé), callback (GET safe, state
comparé en temps constant, tokens stockés puis cookies purgés, redirection
?oauth=connected / ?oauth_error=), disconnect. OpenAPI 523 chemins.
- Config + .env.example : GOOGLE_CLIENT_ID/SECRET, MS_CLIENT_ID/SECRET (vidés =
« non configuré »), redirect URI dérivé d'APP_BASE_URL.
- Catalogue : google/ms365 en natifs avec badge connecté/non connecté ;
connector_fetch et Tester passent par l'API du fournisseur avec le token de
l'utilisateur (user_id transmis par l'outil LLM).
- Menu + : « Se connecter » / « Déconnecter » sur la fiche, toast au retour du
flux (URL nettoyée par history.replaceState). État dans la fiche du menu
plutôt qu'une page dédiée.
- Tests : tests/test_v756_oauth_connectors.py (13), 0 appel réseau réel
(_post_form / _api_get monkeypatchés) — state forgé refusé sans échange,
tokens chiffrés en base, refresh, URL absolue refusée, 401/404, câblage menu ;
test_v755 adapté (5 natifs). Suite complète 1319 verts (-n auto), ruff 0,
eslint 0 erreur (19 warnings préexistants hors fichiers touchés).
|
||
|
|
18c72d77fe |
feat: menu + — Connecteurs (socle) — phase 5/8 (v7.55.0)
- 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. |
||
|
|
64c85caffc |
feat: menu + — Mémoire de l'agent — phase 4/8 (v7.54.0)
- 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. |
||
|
|
a68e8848ea |
feat: menu + — Design System – Canevas — phase 3/8 (v7.53.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.
|
||
|
|
18a2d45d46 |
feat: menu + — Compétences-skills « Gérer » + « Parcourir » — phase 2/8 (v7.52.0)
- Sous-menu « Compétences-skills » : 11 skills builtin + skills enregistrés,
libellés affichés « Deep research » (/research) et « Skill-creator »
(/create-new-skill) via FD_SKILL_ALIAS (slugs intacts) ; le clic épingle le
skill au contexte, même chemin que la palette /.
- « Gérer les compétences » : section du menu + formulaire intégré (nom,
description, prompt) — création, édition, suppression (chip épinglé nettoyé),
export JSON téléchargé, import fichier avec overwrite (pas de doublon).
- « Parcourir les compétences » : section galerie filtrable (nom/description/
slug), installation en 1 clic (installGallerySkill réutilisé).
- PATCH /api/agent/skills/{skill_id} créé : édition partielle, whitelist de
colonnes, 400/401/404/409. OpenAPI régénéré : 515 chemins, version 7.52.0.
- En-tête ← + titre de section dans le menu (navigation à 3 niveaux).
- Fix : épingler un skill enregistré n'efface plus le message déjà tapé
(_pinDbSkill ne vide le composer que s'il est vide).
- Tests : tests/test_v752_skills_menu.py (7) + test_v751 adapté. Suite
complète 1280 verts (-n auto), ruff 0, eslint 0 problème.
|
||
|
|
f271ac9b7a |
feat: menu + de l'assistant en hub de contexte — phase 1/8 (v7.51.0)
- Bouton + : menu à sections (fichiers/répertoires, compétences-skills,
connecteurs, Design System – Canevas, Add plugins, Mémoire on/off).
Sections pas encore livrées affichées « bientôt (phase N) » mais désactivées,
navigation clavier ↑/↓/Entrée/Échap, focus visible, la frappe referme le menu.
- Parcours « Parcourir… » : un niveau par appel via GET /api/nav/menu
(contrat : dossier = icon 'folder'), fil d'Ariane cliquable, épingle de dossier
via la ligne « 📌 Épingler le dossier ».
- Jeton folder:<id> résolu par ContextBuilder._single_folder() : titre du dossier
+ documents directs, budget ~12k caractères (marqueur « tronqué »), enfants
directs seulement.
- « Rechercher… » conserve l'ancien sélecteur de mentions (@ inline + recherche
par nom) : régression zéro sur le chemin existant.
- Tests : tests/test_v751_plus_menu.py (7 tests). Suite complète 1273 verts
(-n auto), ruff check app tests propre, eslint static/js 0 erreur.
- Docs : CHANGELOG, ROADMAP (phase 1 cochée), docs/V74_Agent_Plus_Menu.md statut.
|
||
|
|
81746d9440 |
fix: peek de Library nu comme local-workspace - iframe pleine (v7.50.2)
.peek-body gardait padding:16px 20px + overflow-y:auto (heritage de l'ancien peek a rendu HTML) : l'iframe etait amochee (519x816 mesures contre 559x848 en local-workspace) avec bande grise autour du document + double scrollbar. Corps nu (padding:0, overflow:hidden) : l'editeur remplit le panneau, exactement comme le peek de /local-workspace. Mesure Playwright (cookie de session forge, les 14 boutons Open de Library) : embed OK partout, bases hydratees, aucun jeton « Loading database... ». Test: test_library_peek_body_has_no_inset_padding. Suite 1266/0 (-n auto) + ruff 0. Bump VERSION/main.py 7.50.2, CHANGELOG, WORKLOAD, OpenAPI. |
||
|
|
f53205bc45 |
fix: peek des bases hydrate FlowDeckDB + Open de ligne, tableau pleine largeur (v7.50.1)
- page_editor_embed.html incluait seulement _page_editor_scripts.html : window.FlowDeckDB etait undefined dans l'iframe -> une base ouverte en side peek restait a « Loading database... ». Include _database_table_scripts.html ajoute (meme traitement que page_editor.html). - Le panneau #db-side-peek n'existait que dans _database_table.html : hors page dediee, openPageInSidePeek etait un no-op. Desormais cree a la demande par ensureDbPeek (database_table.js) ; markup unique retire du partial. - .database-table-container : plafond 1100px retire (regle heritee de Library) + max-width 100% pour .db-first et database-page-title : le tableau prend la largeur du document, plus de barre de scroll parasite sous le tableau. - Tests: test_embed_editor_can_render_inline_databases. Suite 1265/0 (-n auto) + ruff 0. Bump VERSION/main.py 7.50.1, CHANGELOG, WORKLOAD, OpenAPI. |
||
|
|
3706689eca |
fix: side peek = document seul en edition + bouton Open calendrier/Kanban (v7.50.0)
- Les 4 peeks (library.js, local_workspace.js, my_tasks.js, database_table.js)
chargent /pages/{id}?embed=1 : le panneau ne montre plus que le document en
mode edition, sans sidebar ni barre (regression v7.49.0). Le cadre du
panneau (close/full/resize partages fdWirePeekResize) est inchange.
- Calendrier My Tasks : le handler testait e.target.dataset.openRow mais les
boutons .mt-cal-open ne contiennent que leur <svg> -> clic sur l'icone =
e.target = <svg>, rien ne partait. cause racine corrigee par
t.closest('[data-open-row]') : un seul chemin tableau/calendrier/Kanban.
- Kanban My Tasks : meme bouton Open ajoute sur les tuiles (openButton(t),
overlay absolu revele au survol de la carte .mt-card).
- Menu contextuel global « Open in side peek » : etait un toast d'erreur ;
route maintenant vers le peek de la page courante (_libData.openPeekById /
_wsData.openSidePeek), repli nouvel onglet ailleurs.
- /pages/{id}?embed=1 sur une page de base (content_format=collection)
rendait un editeur de blocs vide : le template collection est garde en
embed (body.embed-mode retire sidebar+barre, le tableau reste). Bouton
pleine page des peeks de bases : navigue via dataset.page (URL propre)
au lieu du src ?embed=1.
- Tests : garde de coherence inversee (?embed=1 exige sur les 4 fichiers),
+3 nouveaux (closest, tuiles Kanban, collection embed). Fix d'un test
pre-existant casse (assertion panel.style.display, reecriture vanilla JS
v7.49.0). Suite 1264 passed / 0 failed (-n auto) + ruff 0.
- Bump VERSION/main.py 7.50.0, CHANGELOG, WORKLOAD, OpenAPI regenere.
|
||
|
|
1d1cdbd618 |
fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient loovverture et le redimensionnement inoperants -> cblage direct sur le document. - Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw, clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks. - database-table-container margin:0 (tableau colle a gauche, marge Library). - .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px. - ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186). |
||
|
|
6582df3bcb |
feat: poubelle flexible (sélection groupée, purge, tri/filtres, dates) + 4 bugs (v7.45.5)
Page /trash alignée sur ce que fait ce type de section :
- sélection multiple (Select all porté au filtre courant) + barre d'actions
groupées Restore / Delete / Clear ;
- Empty Trash via POST /board/api/trash/empty (purge en masse, 1 requête) ;
- tri Recently deleted / Oldest first / Name et filtre emplacement RÉELS —
remplacent les deux boutons décoratifs « Last edited by ▾ » et « In ▾ » qui
ne faisaient rien ;
- date de suppression + jours restants par élément (rétention 30 j alignée sur
app/services/trash.py, horodatages UTC/ISO gérés) ;
- compteur de résultats, états vides distincts (vide vs filtre sans résultat +
Clear filters), toasts sur chaque action.
Bugs trouvés en route et corrigés :
1. Restore/Delete de la page ne marchaient JAMAIS : getCsrf() renvoie la chaîne
du jeton mais le code faisait csrf?.[1] → 2e caractère → 403 CSRF silencieux
avalé par if (r.ok).
2. « Move to Trash » de l'éditeur = 404 permanent : route appelée
/board/api/pages/{id}/trash (inexistante) alors que la route réelle est
/api/pages/{id}/trash, et le .then() naviguait quand même → la page partait
à l'accueil SANS être mise à la poubelle. URL corrigée + r.ok vérifié.
3. Page restaurée invisible en Library/Recents/Private : la suppression éditeur
réécrivait parent_section='Trash' et la restauration ne le remettait pas,
alors que tous les listings filtrent parent_section != 'Trash'. Écriture
retirée (deleted_at = source de vérité unique) + réparation idempotente au
boot dans db.init_db + requête sidebar /trash alignée sur deleted_at.
4. Routes trash sans aucune authentification (le CSRF ne protège pas : cookie
lisible + en-tête forgé) : 401 ajouté sur list/restore/delete/empty et sur
POST /api/pages/{id}/trash — vérifié anonyme → 401.
Gates : tests/test_trash_api.py (5) · e2e/trash_ui.spec.js (1, avec garde « on
ne vide jamais la poubelle d'autrui ») · pytest 1099 passed · ruff OK ·
e2e probe_nav_perf + partial_nav + editor_mount + logout_dnd + smoke = 10 passed ·
OpenAPI 510 chemins / 7.45.5.
|
||
|
|
de751ffe35 |
feat: A20 TERMINÉ — Alpine en build CSP, unsafe-eval retiré de la CSP (v7.43.0)
La bascule A20 phase 3 : - static/js/alpine.csp.min.js (build officiel @alpinejs/csp, 0 eval/new Function, parseur maison) servi partout : base.html, import.html, welcome.html + entree sw.js (cache bump v8). - CSP : script-src 'self' 'nonce-…' — unsafe-eval SUPPRIMÉ (ne servait plus qu'Alpine standard). htmx allowEval:false deja pose (v7.37). - Assertion test inversée : assert "'unsafe-eval'" not in script_src. - Scan statique final sur TOUS les templates : 0 expression incompatible (4 residus = faux positifs dans des chaines de texte). Pré-requis réunis par les lots 1-3 : 12 surfaces migrées + gateées (csp_preview), registres Alpine.data, x-html → x-init+Alpine.effect, délégués window.E, partage d'état lexical, bug topbar corrigé. Verifs : suite **1094/1094** · ruff OK · eslint 0/0 · **E2E 7/7 sous CSP reel** (script-src sans unsafe-eval verifie sur l'instance). Hors gate (scan propre, gitea down) : board/table_view/teamload/ card_detail → à vérifier au premier usage avec gitea remonté (noté ROADMAP/CHANGELOG). |
||
|
|
48b5551b93 |
fix: BUG TOPBAR — boutons du header servis échappés sur toutes les pages (v7.42.0)
Cause racine : {% set right_actions = '…' ~ fd_icon(…) ~ '…' %} —
fd_icon est une macro → Markup, et Markup.__radd__/__add__ ÉCHAPPE ses
arguments str → tous les segments littéraux sortent entité-és ("/<),
et le |safe de _header:141 est no-op sur un Markup déjà échappé.
Régression probable depuis A10 (activation d'autoescape).
Fix (5 templates, forme idiomatique) : conversion en block-set
{% set right_actions %}…{{ fd_icon(…) }}…{% endset %} — source brute,
interpolation Markup brute : gitea_workspace, page_editor,
page_editor_collection, workspace, workspaces. (Piège script : regex
greedy multi-lignes avalait le set suivant → matcher sur UNE ligne.)
Tests :
- NOUVEAU tests/test_topbar_right_actions.py (permanent) : /workspaces
doit servir class="topbar-btn" parsé et ZÉRIE entité "
- gate éditeur CSP : assertion .star-btn RÉTABLIE (les boutons rendent)
- debug temporaires (DBGCLS/DBGVAL) retirés
suite **1094/1094** (+1 nouveau test) · ruff OK · E2E **7/7** (5
csp_preview + 2 smoke) · docs à jour (CHANGELOG détail, ROADMAP bug →
CORRIGÉ)
|
||
|
|
840d2b2615 |
feat: A20 — htmx allowEval off + plan Alpine CSP phase 3 scopé par probes (v7.37.0)
Changed :
- htmx `allowEval: false` dans le meta htmx-config (base.html) : plus
d'évaluation JS côté htmx (hx-on/hx-vars/hx-vals = 0 usage grep → zéro
régression possible) ; unsafe-eval reste UNIQUEMENT pour Alpine standard.
- Gate E20 renforcée : le smoke vérifie que `Alpine.$data()` lie un vrai
composant [x-data] de la page (lien composant = cœur de toute bascule CSP).
- sw.js : cache bump flowdeck-v7 (purge + re-precache après Inter).
Probes (non conservés, retirés après mesure) — A20 phase 3 scopée :
- Build `@alpinejs/csp` téléchargé et TESTÉ : 72 Ko, 0 eval/new Function,
parseur d'expressions maison, tourne sous CSP strict (meta sans
unsafe-eval) — le lint sélectif fonctionne.
- Mais bloqué sur FlowDeck :
(a) 13 expressions non parsables par la grammaire restreinte
(arrows ×2, typeof ×1, new Date ×4, optional-chaining ×6 ;
base, library, local_workspace, settings, gitea_workspace) —
le gate E2E a attrapé la première : `CSP Parser Error: Unexpected
token: PUNCTUATION ")"` ;
(b) 24 `x-html` réactifs (icônes SVG + markdown agent + preview) =
INTERDITS par le build CSP (innerHTML) → architecture d'icônes à
reposer ;
(c) scope des expressions CSP = données du composant uniquement
(probe : `Undefined variable: fmtDate` / `document`) → chaque site
devient une méthode Alpine.data enregistrée.
- Conséquence : build CSP reverté (alpine.min.js ×3 templates + sw),
unsafe-eval maintenu, fichier alpine.csp.min.js retiré (re-téléchargeable),
assert test CSP de nouveau `in`. Plan de migration composant par composant
(library → settings → local_workspace → gitea → base) + gate E2E par
surface documenté dans ROADMAP (A20 phase 3).
suite **1093/1093** · ruff OK · E2E **2/2** (dont assertion Alpine.$data)
· docs à jour (ROADMAP A20 phase 3, CHANGELOG, WORKLOAD, VERSION)
|
||
|
|
ab6ac1e84c |
feat: fondations E2E + 2 bugs trouvés (onglets ?view=, Inter CSP) (v7.36.0)
Ajout — e2e/smoke.spec.js (2 gates verts contre l'instance de test) : - gate A39 : bascule de vues d'une collection (clic onglet Calendar → ?view_type=calendar, grille .calendar + .cal-header rendue ; collection créée puis SUPPRIMÉE = répétable) - gate A20 : palette Ctrl+K (ouverture Alpine .open, recherche GET rend .cmd-palette-item, fermeture Échap) - filet console : 0 erreur JS/CSP (bruit Failed to load resource 401/403 filtré) - Service Workers bloqués : /sw.js sert sa page « hors ligne » sur les navigations redirigées (redirect:'manual') — pwa_offline.spec.js couvre le SW - bootstrap autonome : login OU création du compte e2e documenté (jamais de mot de passe deviné), workspace si absent - commande : cd e2e && node node_modules/@playwright/test/cli.js test smoke.spec.js Fixed — trouvés par les gates : 1. Bascule de vues standalone JAMAIS fonctionnelle : les onglets émettaient ?view=… mais la route lit `view_type` (FastAPI) → l'onglet restait sur Table quel que soit le clic (bug pré-existant, A28 n'y est pour rien). Onglets → ?view_type= ; test_all_view_tabs_present adapté + assertion comportementale (GET ?view_type=calendar rend .calendar). 2. Inter bloqué par la CSP depuis v7.27 : app.css importait encore Google Fonts (@import raté par le grep de la passe v7.27) → violation style-src sur chaque page + police en fallback. Inter auto-hébergé : 2 faces variables (100-900, latin + latin-ext) dans static/fonts/, @import supprimé (8 fichiers dupliqués dédupliqués → 2). suite **1093/1093** · ruff OK · E2E **2/2** · docs à jour |
||
|
|
13dc8fdaad |
fix: A38 phase 2 — 0 doublon de fonction globale + garde-fou (v7.34.0)
- 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 |
||
|
|
770fdc2b68 |
fix: A43 TERMINÉ + A38 phase 1 — CSRF rendu côté serveur, helper unique (v7.33.0)
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
|
||
|
|
adf56a2dd8 |
refactor: A28 lot 3 — collections.py (2 622 L) → package 13 fichiers (v7.31.0)
Découpe par concern de l'ancien app/routers/collections.py (2 622 lignes, 53 endpoints / 52 fonctions) en package `app/routers/collections/` : - 10 modules de routes : crud 337 L (6 r.), properties 322 (8), linked 286 (7), structure 267 (8), dashboard_views 214 (3), meta 197 (5), views 187 (6), pages 184 (4), data_api 122 (2), boards 61 (3) - _common.py (220 L) : 8 helpers auth/permissions/validation - _renderers.py (667 L) : 15 rendus HTML des vues + CHART_MAX_GROUPS - __init__.py : ré-exports connus (_validate_page_properties pour automations ; _chart_values/_chart_aggregate/_fmt_number/_render_chart pour les tests) + __all__ Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE byte-à-byte (509 chemins, ordre préservé). Pièges rattrapés : - docstring d'origine conservée dans le header copié → F404 (from __future__ après un statement) → slice [1:30] - décorateurs empilés (view_collection ×2) : segment sans def → skip du 2e décorateur (53 endpoints = 52 unités) - CHART_MAX_GROUPS hors détection des helpers (F821) → import ._renderers - test_csp_no_cdn_and_vendor lisait collections.py → balayage du package Reste A28 : board.py 2 101 L (lot 4). suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour |
||
|
|
3bb8e87ef2 |
fix: A42 terminé — client httpx partagé par boucle (v7.28.0)
- `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 |
||
|
|
069c438aae |
fix: A20 phase 2 — chart/leaflet vendorisés + connect-src fermé (v7.27.0)
- 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
|
||
|
|
103bc57418 |
fix: A27 phase 2c — database_table 1 314 L, extraction A27 terminée (v7.24.0)
- `_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
|
||
|
|
ee1d46e965 |
fix: A27 phase 2a — éditeur 2 516 L extrait via page-data JSON (v7.22.0)
- `_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
|
||
|
|
7a38ddd0f6 |
test: A32 TERMINÉ — 6 routes Gitea stubbées + bug prod fd_icon (v7.20.0)
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
|
||
|
|
113374e499 |
test: A32 phase 2h — dashboard bloqué : 44/44 routes à 0 ref (v7.19.0)
+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
|
||
|
|
0cb476e336 |
test: A32 phase 2g — dashboard +13 routes, cycles items/tags (v7.18.0)
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
|
||
|
|
2339fa2586 |
test: A32 phase 2f — dashboard +7 routes, garde-fous A16 (v7.17.0)
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
|
||
|
|
8b48dbdd4b |
test: A32 phase 2e — dashboard +9 routes, comptes A2/A3 (v7.16.0)
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
|
||
|
|
360c705fd4 |
test: A32 phase 2d — dashboard +10 routes couvertes (v7.15.0)
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
|
||
|
|
0698645dbd |
test: A32 phase 2c — api_v2 : les 5 routes à 0 ref couvertes (v7.14.0)
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 |
||
|
|
b2e38aece7 |
test: A32 phase 2b — api.py 3 → 16/22 routes couvertes (v7.13.0)
+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 |
||
|
|
df9a269d76 |
test: A32 phase 2a — library 10/10 + 2 routes fantômes supprimées (v7.12.0)
- 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
|
||
|
|
da7326ffde |
test: A32 phase 1 — 4 routers à 0 test couverts (10 smokes) (v7.11.0)
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
|
||
|
|
c718fe06de |
fix: A20 (partiel) — CSP nonce par requête, unsafe-inline sort de script-src (v7.7.0)
- 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
|
||
|
|
f706424f90 |
fix: A31 — transaction par migration + helper columns() (v7.6.0)
- `_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
|
||
|
|
7be96f0618 |
fix: A29 + A42(partiel) — publish partagé, fuite password_hash, data_dir (v7.5.0)
- A29 — `app/services/publish.py` : slugify titré unique (fallback aléatoire),
404 si la page n'existe pas, événements centralisés. Les 3 paires
publish/unpublish déléguent (sharing = front, board, v2) :
· board : mise à jour aveugle → 404 + contrôle de session ajouté
· board : perd `share_mode='anyone'` en bonus, v2 : perd `is_shared=1` —
le share dialog reste l'unique propriétaire de ces drapeaux
· v2 : slug fourni conservé, slug vidé aussi à la dépublication (avant : laissé)
· `/users/me` ×2 et listings collections ×3 = contrats versionnés distincts,
décision documentée (on garde)
- Byproduct sécurité — `GET /api/users/me` (v1) et le contexte de `/accounts`
faisaient `SELECT *` sur users → password_hash / login_attempts / locked_until
exposés → colonnes whitelistées (liste v2)
- A42 (partiel) — 9 copies de `Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))`
→ `settings.data_dir` (property : lecture à chaque accès, les tests
monkeypatchent l'env) ; cache Gitea : évacuation des entrées expirées à chaque
écriture. Reste : client httpx partagé (52 créations, cache par event loop)
tests : test_publish_service_shared_and_safe, test_users_me_no_secret_columns,
test_gitea_cache_evicts_expired
suite **1034/1034** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.5.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
|
||
|
|
937ecfc2e0 |
fix: A30 + A37 + A39 + A40 + A41 — fin du P2/P3 XS/S (v7.4.0)
- A30 — `require_scope()` câblé : 69 sites stricts de api_v2.py passent par la factory (Bearer + scope en 1 appel, contrôle manuel supprimé) ; sémantique alignée sur celle des handlers (pas de default "read" → 0 changement de comportement) ; 12 top-level morts supprimés (0 ref app ET tests) : unsync_block, find_referring, _b64url, strip_markdown, format_number, get_auto_property_value, get_next_unique_id, local_date_in_tz, verify_device_token, _get_dynamic_groups, _require_user_gitea, validate_upload_request - A37 — CORS sans `*` : origines = app_base_url + allow_origin_regex (localhost/dev, origines d'extension pour le Web Clipper), méthodes et entêtes minutées, allow_credentials explicite + test test_cors_no_star - A39 — htmx : décision « rien » documentée (32 attributs hx-* réels sur 6 templates, conversion = refonte du view-switching sans test E2E) - A40 — version d'assets à source unique : ENV.globals["asset_version"] lu au boot depuis le fichier VERSION ; littéraux `?v=` de base.html éliminés ; test test_asset_version_single_source - A41 — app.css : 91 règles mortes purgées (-10 274 octets, 121 618 → 111 344), scan templates/JS/CSS/Python à 0 référence suite **1031/1031** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.4.0 docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour |
||
|
|
cb47f5c7f4 |
fix: A26 + A33 + A34 + A35 + A36 + A43 — bande S/XS du P2 (v7.3.9)
- 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 |
||
|
|
ffa1fa89ab |
fix: A25 + A21 (partiel) — plus d'exception muque, transaction protégée (v7.3.8)
- 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
|
||
|
|
3ad2605c9e |
fix: A14 — fin du fallback « row admin » sur l'agent (v7.3.7)
- `_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 |
||
|
|
1f705ce512 |
fix: A19 terminé — plus aucun préfixe cookie-auth exempt du CSRF (v7.3.6)
- 46 appels non-GET équipés de `X-CSRF-Token` (expression cookie en ligne, portée indifférente) : agent_panel (9), settings (12), local_workspace (15), gitea_workspace (4), workspace (2), workspaces (5), library (2), welcome (2 en v7.3.5) - 5 derniers préfixes sortis d'`EXCLUDED_PATHS` : /api/workspace (+/api/workspaces), /api/local-workspace, /api/settings, /api/gitea, /api/agent - il ne reste exempté que : Bearer (/api/webhook, /api/v1, /api/v2, /scim/v2), callbacks /auth/*, pages publiques (/s/, /f/), /api/csrf-token et /api/frontend-error - vérif : `node --check` des <script> des 39 templates → 0 échec (avant et après) - tests : `anon_csrf()` sur les 5 tests d'anonymat devenus CSRF-first - suite **1026/1026** · `ruff check app tests` OK |
||
|
|
0861f1fdbf |
fix: A19 (partiel) — CSRF réservé aux vrais cas d'exemption (v7.3.5)
- 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 |
||
|
|
72fcef2ba9 |
fix: A16 — ACL sur l'export et les pièces jointes (v7.3.4)
- `export.py` : `_load_page_or_404(request, page_id)` — 401 sans session,
404 si `PermissionManager.can_view_page` refuse (les 4 formats d'export)
- `dashboard.py` : helper `_require_page_view` sur `GET /api/pages/{id}/download`
et `GET /api/pages/{id}/file-content`
- +1 test de non-régression → suite **1026/1026**, `ruff check app tests` OK
|
||
|
|
5a537f5dc3 |
fix: A12–A24 — SSRF, auth routes legacy, uploads, N+1 et routes doublonnes (v7.3.3)
- A12 — `og_fetcher` : GET sans `follow_redirects`, `_is_public_host` revérifié à chaque saut (max 5) ; `POST /board/api/og/metadata` → 400 sur hôte privé/loopback - A13 — router automations sous `Depends(_require_session)` (CRUD, run, press-button) + `created_by` sans fallback ; action `webhook` validée par `_is_public_host` avant POST (SSRF) - A15 — webhooks sortants : `_require_admin` sur GET/POST/DELETE + `_is_public_host` sur l'URL en création - A17 — router legacy `/api` sous `Depends(_require_session_or_bearer)` (session ou Bearer `/api/v1`), allowlist explicite `/api/health` + `/api/frontend-error` - A22 — les 2 uploads locales : session exigée (`_require_user_id`) + `validate_upload` branché (taille + extension) + `FLOWDECK_DATA_DIR` au lieu de `/data` codé en dur - A23 — N+1 : COUNT→`GROUP BY` (dashboard), cards→`executemany` (board sync), duplicata de propriétés→`executemany` + remap des ids par SELECT (collections) - A24 — 2 routes écrasées supprimées : `GET /api/projects` (api.py) et `GET /workspace` (workspace.py) + test « aucun doublon méthode+chemin » - Tests : +9 dans `tests/test_audit_p0_fixes.py` (SSRF, 401s, validate_upload, doublons de routes) ; tests OG sur hôtes résolubles (la garde fait du DNS) - suite **1025/1025** · `ruff check app tests` OK |
||
|
|
8ab6569974 |
fix: A11 + A18 — path traversal avatar et XSS/flags sur la vue publique (v7.3.2)
- 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
|