diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b59a2c..cd849b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog - FlowDeck +## v7.53.0 (2026-10-06) — Menu + : Design System – Canevas (phase 3/8) + +### Added + +- **Section « Design System – Canevas »** — liste des canevas + (`GET /board/api/page-templates` : intégrés + personnels), puis une page de + détail avec 3 actions : + - **Créer une page à partir du canevas** (`POST …/0/use` avec `{key}` *dans le + body* pour un intégré, `POST …/{id}/use` pour un personnel) ; + - **Insérer dans le document ouvert** — `GET …/blocks` (nouvelle route) → + `ensureBlockIds()` (global côté éditeur) → concaténation dans `E.blocks` + + `autoSave/render`, désactivé si aucun document éditable n'est ouvert ; + - **Enregistrer le document ouvert comme canevas** + (`POST /board/api/page-templates` avec `page_id`). +- **`GET /board/api/page-templates/{template_id}/blocks`** — blocs d'un canevas + ( intégré via `template_id=0&key=`, sinon id) ; 404 clé/id inconnu, + 500 si le JSON est corrompu. **OpenAPI : 516 chemins**, `info.version` 7.53.0. +- **Canevas « Design System »** intégré (`design_system` dans + `app/services/block_templates.py`) : callout tokens, TOC, 3 toggles + composants, grille & espacement, checklist revue UI, citation. +- **Navigation** : en-tête ← gère le niveau canevas → liste. + +### Fixed + +- **Prefixe des canevas** : le front du panneau utilisait `/api/page-templates` + (404 silencieux) — la route réelle est `/board/api/page-templates` (router + `prefix="/board"`), comme `base.html` le fait déjà. + +### Tests + +- `tests/test_v753_canvases.py` — **5 tests** : canevas `design_system` listé, + route blocks intégré (sans `id`, 404 clé inconnue, `empty` par défaut), + route blocks personnel + 404, création de page depuis le canevas (blocs lus + en base, ids assignés), câblage des sections (3 « bientôt » restantes) + + garde-fou « `key` dans le body ». `test_v751` adapté (3 sections désactivées). +- **Suite complète : 1285 passed / 0 failed** (`-n auto`), `ruff` 0, + `eslint` 0 problème. + + ## v7.52.0 (2026-10-06) — Menu + : Compétences-skills « Gérer » + « Parcourir » (phase 2/8) ### Added diff --git a/ROADMAP.md b/ROADMAP.md index d672c1e..594dc9c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -284,7 +284,7 @@ Propriétés custom, AI keywords, sync API, 12 tables DB --- -## v7.51.0 → v7.58.0 — Menu + de l'assistant : hub de contexte (phases 1-2 ✅ 2026-10-06 · phases 3-8 planifiées) +## v7.51.0 → v7.58.0 — Menu + de l'assistant : hub de contexte (phases 1-3 ✅ 2026-10-06 · phases 4-8 planifiées) > **Objectif** : faire du bouton **+** du panneau agent (à gauche de la zone d'édition) > un menu à sections, tel que demandé : @@ -366,16 +366,29 @@ CHANGELOG). Chiffrage : S < ½ j · M = 1–2 j · L = 3–5 j. - [x] **Livraison** — `VERSION` + `app/main.py` = **7.52.0**, OpenAPI régénéré (515 chemins), CHANGELOG, ce ROADMAP -### Phase 3 — v7.53.0 — Design System – Canevas · effort M +### Phase 3 — v7.53.0 — Design System – Canevas · effort M ✅ (livrée 2026-10-06) -- [ ] **Sous-menu Canevas** — `GET /api/page-templates` (built-ins + personnels) -- [ ] **Deux actions** — « Créer une page à partir du canevas » - (`POST /api/page-templates/{id}/use`) et « Insérer dans le document ouvert » - (blocs poussés dans l'éditeur quand un document est ouvert) -- [ ] **Canevas « design system »** — presets maison dans `block_templates.py` - (callout, TOC, colonnes, grille de composants) réutilisant `design-tokens.css` -- [ ] **Sauver le document ouvert comme canevas** — `POST /api/page-templates` existe -- [ ] **Tests** — création, insertion dans l'éditeur, sauvegarde +- [x] **Sous-menu Canevas** — `GET /board/api/page-templates` (built-ins **+ + personnels**) ; **piège confirmé** : le router est sous `prefix="/board"` → + l'URL réelle est `/board/api/page-templates`, pas `/api/page-templates` + (404 silencieux corrigé dans le front) +- [x] **Deux actions (3 en réalité)** — page de détail par canevas : + « Créer une page à partir du canevas » (`POST …/0/use` avec `{key}` **dans le + body** — la clé en query est ignorée par la route), « Insérer dans le document + ouvert » (`GET …/blocks` **nouvelle route** → `ensureBlockIds()` global → + concat dans `E.blocks` + `autoSave/render`, désactivé sans document ouvert) + et « Enregistrer le document ouvert comme canevas » (`POST` + `page_id`) +- [x] **Canevas « design system »** — preset `design_system` dans + `block_templates.py` (callout tokens, TOC, toggles composants, grille, + checklist revue UI) ; `design-tokens.css` référencé dans le callout +- [x] **Sauver le doc ouvert comme canevas** — `POST /board/api/page-templates` + (existante) branchée depuis le menu, nom via `prompt()` natif +- [x] **Tests** — `tests/test_v753_canvases.py` : **5 tests** (preset listé, + route blocks intégré/personnel + 404, création de page vérifiée en base avec + ids, câblage 3 « bientôt » + garde-fou `key`-dans-le-body) ; `test_v751` + adapté ; **suite complète 1285 verts**, `ruff` 0, `eslint` 0 problème +- [x] **Livraison** — `VERSION` + `app/main.py` = **7.53.0**, OpenAPI régénéré + (516 chemins), CHANGELOG, ce ROADMAP ### Phase 4 — v7.54.0 — Mémoire de l'agent · effort M @@ -429,9 +442,10 @@ CHANGELOG). Chiffrage : S < ½ j · M = 1–2 j · L = 3–5 j. --- -*Plan produit le 2026-10-06 à partir du code réel — **phases 1 et 2 livrées le -2026-10-06 (v7.51.0, v7.52.0)**, phases 3-8 à valider une par une avant « go ». -Chaque phase se clôt par tests verts, bump de version, CHANGELOG et ROADMAP à jour.* +*Plan produit le 2026-10-06 à partir du code réel — **phases 1 à 3 livrées le +2026-10-06 (v7.51.0, v7.52.0, v7.53.0)**, phases 4-8 à valider une par une avant +« go ». Chaque phase se clôt par tests verts, bump de version, CHANGELOG et +ROADMAP à jour.* --- diff --git a/VERSION b/VERSION index 4cfd27f..b02dd46 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.52.0 +7.53.0 diff --git a/app/main.py b/app/main.py index 85f508c..3d01495 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.52.0", + version="7.53.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/routers/board/page_api.py b/app/routers/board/page_api.py index f56e2d0..22569d0 100644 --- a/app/routers/board/page_api.py +++ b/app/routers/board/page_api.py @@ -226,4 +226,38 @@ def use_page_template(request: Request, template_id: int, body: dict = Body(defa return {"status": "ok", "id": page_id, "title": title or name} +@router.get("/api/page-templates/{template_id}/blocks") +def page_template_blocks(request: Request, template_id: int, key: str = ""): + """v7.53.0 : blocs d'un canevas, pour l'insérer dans le document ouvert. + + Builtin : ``template_id=0`` + ``?key=`` (même convention que ``/use``). + Canevas personnel : son ``id``. Les blocs builtin sont sans ``id`` — le + front appelle ``ensureBlockIds()`` (déjà global côté éditeur). + """ + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if template_id == 0: + from app.services.block_templates import blocks_json_for + raw = blocks_json_for(key or "empty") + if raw is None: + raise HTTPException(404, "Unknown built-in template") + else: + uid = (user or {}).get("id") + with get_conn() as conn: + row = conn.execute( + "SELECT blocks_json FROM page_global_templates " + "WHERE id=? AND (created_by IS NULL OR created_by=?)", + (template_id, uid), + ).fetchone() + if not row: + raise HTTPException(404, "Template not found") + raw = row["blocks_json"] + try: + blocks = json.loads(raw) + except (json.JSONDecodeError, TypeError) as exc: + raise HTTPException(500, "Template content corrupted") from exc + if not isinstance(blocks, list): + raise HTTPException(500, "Template content corrupted") + return {"blocks": blocks} + + # ── Core helpers ── diff --git a/app/services/block_templates.py b/app/services/block_templates.py index 89c8ce8..3e4e670 100644 --- a/app/services/block_templates.py +++ b/app/services/block_templates.py @@ -86,6 +86,34 @@ BUILTIN_TEMPLATES: dict[str, dict] = { _b("bulleted_list"), ], }, + "design_system": { + "name": "Design System", + "icon": "🎨", + "description": "Tokens, composants et grille — canevas de référence UI.", + "blocks": [ + _b("heading_1", "Design System"), + _b("callout", + "Tokens : couleur, espacement, typographie, rayon, ombres " + "(static/css/design-tokens.css).", + icon="🎨"), + _b("table_of_contents"), + _b("heading_2", "Composants"), + _b("toggle", "Boutons & actions", expanded=False, + children=[_b("paragraph", "Primaire · secondaire · danger — états idle, hover, focus, disabled.")]), + _b("toggle", "Cartes & panneaux", expanded=False, + children=[_b("paragraph", "Élévation, rayon, bordure, padding, zones de clic.")]), + _b("toggle", "Formulaires", expanded=False, + children=[_b("paragraph", "Champs, labels, hints, erreurs, focus visible.")]), + _b("heading_2", "Grille & espacement"), + _b("bulleted_list", "Base 4 px — pas d'espacement hors échelle."), + _b("bulleted_list", "Colonnes : 12 / 8 / 4 selon le point de rupture."), + _b("heading_2", "Revue UI"), + _b("to_do", "Contraste AA vérifié", checked=False), + _b("to_do", "États hover / focus / disabled", checked=False), + _b("to_do", "Responsive mobile", checked=False), + _b("quote", "Une décision de design vaut mieux qu'une justification après coup."), + ], + }, } diff --git a/docs/V74_Agent_Plus_Menu.md b/docs/V74_Agent_Plus_Menu.md index 52b4672..6b091ba 100644 --- a/docs/V74_Agent_Plus_Menu.md +++ b/docs/V74_Agent_Plus_Menu.md @@ -1,10 +1,12 @@ # V74 — Menu + de l'assistant : hub de contexte (design) -> **Statut** : design — **phases 1 et 2 livrées (v7.51.0, v7.52.0, 2026-10-06)** ; -> phases 3-8 restent à livrer. Adaptations faites en cours de route : le parcours +> **Statut** : design — **phases 1 à 3 livrées (v7.51.0 → v7.53.0, 2026-10-06)** ; +> phases 4-8 restent à livrer. Adaptations faites en cours de route : le parcours > utilise `GET /api/nav/menu?parent_id=` (un niveau par appel, contrat -> dossier = `icon === 'folder'`) et la gestion des skills est une **section du menu -> avec formulaire intégré** (pas une modale) — `PATCH /api/agent/skills/{id}` ajouté. +> dossier = `icon === 'folder'`), la gestion des skills est une **section du menu +> avec formulaire intégré** (pas une modale — `PATCH /api/agent/skills/{id}` +> ajouté), et les canevas vivent sous **`/board/api/page-templates`** (prefix +> `/board`) avec la nouvelle route `GET …/{id}/blocks` pour l'insertion. > **Plan phasé** : `ROADMAP.md` → section « v7.51.0 → v7.58.0 — Menu + de l'assistant ». > **Date** : 2026-10-06 · Version cible de départ : **v7.51.0** (phase 1). diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 62490a2..d767426 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.52.0" + "version": "7.53.0" }, "paths": { "/auth/register": { @@ -3706,6 +3706,57 @@ } } }, + "/board/api/page-templates/{template_id}/blocks": { + "get": { + "tags": [ + "board" + ], + "summary": "Page Template Blocks", + "description": "v7.53.0 : blocs d'un canevas, pour l'insérer dans le document ouvert.\n\nBuiltin : ``template_id=0`` + ``?key=`` (même convention que ``/use``).\nCanevas personnel : son ``id``. Les blocs builtin sont sans ``id`` — le\nfront appelle ``ensureBlockIds()`` (déjà global côté éditeur).", + "operationId": "page_template_blocks_board_api_page_templates__template_id__blocks_get", + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Template Id" + } + }, + { + "name": "key", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "", + "title": "Key" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/board/library": { "get": { "tags": [ diff --git a/static/js/agent_panel_2.js b/static/js/agent_panel_2.js index 08aa870..346ac9b 100644 --- a/static/js/agent_panel_2.js +++ b/static/js/agent_panel_2.js @@ -42,7 +42,7 @@ {key:'connectors', icon:'🔌', label:'Connecteurs', sub:'Parcourir / connecteur personnalisé — bientôt (phase 5)', disabled:true}, {key:'canvases', icon:'🎨', label:'Design System – Canevas', - sub:'Galerie de canevas — bientôt (phase 3)', disabled:true}, + sub:'Galerie de canevas : créer, insérer, enregistrer', action:'canvases', chev:true}, {key:'plugins', icon:'🧩', label:'Add plugins', sub:'Modules on/off — bientôt (phase 8)', disabled:true}, {key:'memory', icon:'🧠', label:'Mémoire (on/off)', @@ -73,6 +73,7 @@ plusBrowse: [], plusCrumb: [], skillForm: {id: null, name: '', description: '', prompt_template: ''}, galleryFilter: '', + canvases: [], canvas: null, cmdFocus: -1, slashDismiss: false, toastMsg: '', toastErr: false, _toastTimer: null, _livePh: null, @@ -1125,6 +1126,8 @@ if(s === 'skills-manage') return 'Gérer les compétences'; if(s === 'skills-gallery') return 'Parcourir les compétences'; if(s === 'skills-edit') return this.skillForm.id ? 'Modifier le skill' : 'Nouveau skill'; + if(s === 'canvases') return 'Design System – Canevas'; + if(s === 'canvas') return this.canvas ? this.canvas.name : 'Canevas'; return 'Ajouter au contexte'; }, get plusItems(){ @@ -1135,6 +1138,8 @@ if(s === 'skills-manage') return this._manageItems(); if(s === 'skills-gallery') return this._galleryItems(); if(s === 'skills-edit') return []; // formulaire, pas de liste + if(s === 'canvases') return this._canvasItems(); + if(s === 'canvas') return this._canvasActionItems(); return FD_PLUS_MENU; }, // Sous-menu Compétences : builtin (alias affiché) + installés, puis les 2 actions. @@ -1187,6 +1192,106 @@ if(!out.length) out.push({key:'g-none', _sep: f ? 'Aucun résultat' : 'Galerie vide'}); return out; }, + // ── Design System – Canevas (v7.53.0) ── + fetchCanvases(){ + var self = this; + fetch('/board/api/page-templates').then(function(r){ return r.json(); }) + .then(function(d){ self.canvases = d.templates || []; }) + .catch(function(){ self.canvases = []; }); + }, + _canvasItems(){ + return (this.canvases || []).map(function(t){ + return {key: 'cv-' + (t.builtin ? ('b-' + t.key) : ('u-' + t.id)), + icon: t.icon || '📄', label: t.name, + sub: (t.description || (t.builtin ? 'Canevas intégré' : 'Canevas personnel')), + action: 'canvas', canvas: t, chev: true}; + }); + }, + _canvasActionItems(){ + var c = this.canvas; + if(!c) return []; + var pageId = this._openPageId(); + return [ + {key:'cv-meta', _sep: (c.builtin ? 'Canevas intégré · ' : 'Canevas personnel · ') + c.name}, + {key:'cv-create', icon:'+', label:'Créer une page à partir du canevas', + sub:'Nouvelle page dans le workspace courant', action:'canvas-create'}, + {key:'cv-insert', icon:'⇩', label:'Insérer dans le document ouvert', + sub: pageId ? 'Ajoute les blocs en fin de page' : 'Aucun document éditable ouvert', + action:'canvas-insert', disabled: !pageId}, + {key:'cv-save', icon:'⭐', label:'Enregistrer le document ouvert comme canevas', + sub: pageId ? 'Réutilisable depuis ce menu' : 'Aucun document ouvert', + action:'canvas-save', disabled: !pageId} + ]; + }, + // Id de la page ouverte dans l'éditeur (refId du contexte, sinon l'URL). + _openPageId(){ + try{ + var c = window.FlowDeckCtx && window.FlowDeckCtx.current(); + if(c && c.kind === 'page' && c.refId != null && /^\d+$/.test(String(c.refId))) return String(c.refId); + }catch{ /* volontaire */ } + var m = /\/pages\/(\d+)/.exec(window.location.pathname); + return m ? m[1] : null; + }, + _canvasUrl(suffix, t){ + t = t || this.canvas; + return t.builtin + ? ('/board/api/page-templates/0/' + suffix + '?key=' + encodeURIComponent(t.key)) + : ('/board/api/page-templates/' + t.id + '/' + suffix); + }, + canvasCreate(){ + var self = this, c = this.canvas; + if(!c) return; + // le builtin se choisit par `key` DANS LE BODY (route /use), pas en query + fetch(this._canvasUrl('use'), { + method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'}, + body: JSON.stringify(c.builtin ? {key: c.key} : {}) + }).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); }) + .then(function(res){ + if(!res.ok){ self.toast((res.d && res.d.detail) || 'Création impossible.', true); return; } + self.toast('Page créée : « ' + ((res.d && res.d.title) || c.name) + ' » (Workspace).'); + self.plusSection = 'canvases'; self.plusFocus = 0; + }).catch(function(){ self.toast('Création impossible (réseau).', true); }); + }, + canvasInsert(){ + var self = this, c = this.canvas; + if(!c) return; + fetch(this._canvasUrl('blocks')).then(function(r){ return r.json(); }) + .then(function(d){ + var E = window.E; + if(!E || !E.blocks || !d || !Array.isArray(d.blocks)){ + self.toast('Aucun document éditable ouvert.', true); return; + } + if(window.ensureBlockIds) window.ensureBlockIds(d.blocks); + try{ + if(E.sync) E.sync(); + var empty = E.blocks.length === 1 && E.blocks[0].type === 'paragraph' + && !(E.blocks[0].content || '').trim(); + E.blocks = empty ? d.blocks : E.blocks.concat(d.blocks); + E.dirty = true; + if(E.autoSave) E.autoSave(); + if(E.render) E.render(); + self.toast('Canevas inséré dans le document.'); + }catch{ self.toast('Insertion impossible.', true); } + }).catch(function(){ self.toast('Lecture du canevas impossible.', true); }); + }, + canvasSave(){ + var self = this, c = this.canvas; + var pageId = this._openPageId(); + if(!c || !pageId) return; + var def = (this.activeContext && this.activeContext.label) || c.name || 'Mon canevas'; + var name = window.prompt('Nom du canevas à enregistrer :', def); + if(!name || !name.trim()) return; + name = name.trim(); + fetch('/board/api/page-templates', { + method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'}, + body: JSON.stringify({name: name, page_id: Number(pageId)}) + }).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); }) + .then(function(res){ + if(!res.ok){ self.toast((res.d && res.d.detail) || 'Enregistrement impossible.', true); return; } + self.toast('Canevas enregistré : « ' + name + ' »'); + self.fetchCanvases(); + }).catch(function(){ self.toast('Enregistrement impossible (réseau).', true); }); + }, _plusSelectable(){ var out = []; this.plusItems.forEach(function(it, i){ if(!it.disabled && !it._sep) out.push(i); }); @@ -1242,6 +1347,16 @@ if(this.$refs.skillImport) this.$refs.skillImport.click(); return; } + // ── Design System – Canevas (v7.53.0) ── + if(it.action === 'canvases'){ + this.fetchCanvases(); this.plusSection = 'canvases'; this.plusFocus = 0; return; + } + if(it.action === 'canvas'){ + this.canvas = it.canvas; this.plusSection = 'canvas'; this.plusFocus = 0; return; + } + if(it.action === 'canvas-create'){ this.canvasCreate(); return; } + if(it.action === 'canvas-insert'){ this.canvasInsert(); return; } + if(it.action === 'canvas-save'){ this.canvasSave(); return; } if(it._folder){ // entrer dans un sous-dossier this.plusCrumb.push({id: it._folder, name: it.label}); this.plusLoadBrowse(it._folder); @@ -1261,6 +1376,7 @@ if(sec === 'skills-manage' || sec === 'skills-gallery'){ this.plusSection = 'skills'; this.plusFocus = 0; return; } + if(sec === 'canvas'){ this.plusSection = 'canvases'; this.plusFocus = 0; return; } this.plusSection = 'root'; this.plusFocus = 0; }, diff --git a/tests/test_v751_plus_menu.py b/tests/test_v751_plus_menu.py index aee68f9..7f296dd 100644 --- a/tests/test_v751_plus_menu.py +++ b/tests/test_v751_plus_menu.py @@ -104,8 +104,8 @@ def test_plus_menu_sections_and_disabled_states(): for key in ("'files'", "'skills'", "'connectors'", "'canvases'", "'plugins'", "'memory'"): assert f"key:{key}" in root, key assert "action:'files'" in root and "action:'skills'" in root - # phases 1 + 2 livrées → il reste 4 sections « bientôt » - assert root.count("disabled:true") == 4 + # phases 1-3 livrées → il reste 3 sections « bientôt » (connecteurs, plugins, mémoire) + assert root.count("disabled:true") == 3 files = src.split("var FD_PLUS_FILES = [", 1)[1].split("];", 1)[0] assert "action:'search'" in files and "action:'browse'" in files diff --git a/tests/test_v753_canvases.py b/tests/test_v753_canvases.py new file mode 100644 index 0000000..b5c3566 --- /dev/null +++ b/tests/test_v753_canvases.py @@ -0,0 +1,73 @@ +"""v7.53.0 — Menu + : Design System – Canevas (créer / insérer / enregistrer).""" + +import json +from pathlib import Path + +from app.db import get_conn + +ROOT = Path(__file__).resolve().parent.parent +JS_PATH = ROOT / "static" / "js" / "agent_panel_2.js" + + +def test_canvases_list_includes_design_system(client): + tpl = {t["key"]: t for t in client.get("/board/api/page-templates").json()["templates"] if t.get("builtin")} + assert "design_system" in tpl + assert tpl["design_system"]["name"] == "Design System" + assert tpl["design_system"]["icon"] == "🎨" + + +def test_blocks_route_builtin(client): + r = client.get("/board/api/page-templates/0/blocks", params={"key": "design_system"}) + assert r.status_code == 200, r.text + blocks = r.json()["blocks"] + assert isinstance(blocks, list) and blocks + assert blocks[0]["type"] == "heading_1" and blocks[0]["content"] == "Design System" + # les builtins sont livrés sans id : le front appelle ensureBlockIds() + assert "id" not in blocks[0] + + assert client.get("/board/api/page-templates/0/blocks", params={"key": "nope"}).status_code == 404 + empty = client.get("/board/api/page-templates/0/blocks", params={"key": "empty"}) + assert empty.status_code == 200 and empty.json()["blocks"] + + +def test_blocks_route_user_template(client): + blocks = [{"type": "heading_1", "content": "Mon canevas"}, + {"type": "paragraph", "content": "Contenu."}] + created = client.post("/board/api/page-templates", json={"name": "Mon canevas", "blocks": blocks}) + assert created.status_code == 200, created.text + tid = created.json()["id"] + + got = client.get(f"/board/api/page-templates/{tid}/blocks") + assert got.status_code == 200 + assert [b["content"] for b in got.json()["blocks"]] == ["Mon canevas", "Contenu."] + + assert client.get("/board/api/page-templates/999999/blocks").status_code == 404 + + +def test_use_design_system_creates_page(client): + r = client.post("/board/api/page-templates/0/use", json={"key": "design_system"}) + assert r.status_code == 200, r.text + page_id = r.json()["id"] + with get_conn() as conn: + row = conn.execute( + "SELECT content, content_format FROM pages WHERE id=?", (page_id,) + ).fetchone() + assert row["content_format"] == "blocks" + blocks = json.loads(row["content"]) + texts = [b.get("content", "") for b in blocks] + assert "Design System" in texts + # les ids sont assignés côté serveur (_ensure_block_ids) + assert all(b.get("id") for b in blocks) + + +def test_canvases_menu_wired(client): + src = JS_PATH.read_text(encoding="utf-8") + root = src.split("var FD_PLUS_MENU = [", 1)[1].split("];", 1)[0] + assert "action:'canvases'" in root + # phases 1-3 livrées → 3 sections « bientôt » restantes (connecteurs, plugins, mémoire) + assert root.count("disabled:true") == 3 + for action in ("'canvas'", "'canvas-create'", "'canvas-insert'", "'canvas-save'"): + assert action in src, action + # piège /use : la clé builtin part DANS LE BODY, pas en query string + assert "body: JSON.stringify(c.builtin ? {key: c.key} : {})" in src + assert "ensureBlockIds" in src