Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
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É)
Extraction des 7 templates dont le JS n'est PAS interpolé Jinja → 9 fichiers
static/js/*.js (4 243 lignes, -30 % du JS inline : 13 904 → 9 661) :
- agent_panel_1/_2 (bloc de 1 788 L livré sur CHAQUE page), library (1 039),
gitea_workspace (626), _icon_picker_1/_2, _ctx_menu, import, workspaces
- UN fichier par bloc : ordre/timing identiques (pas de defer, attributs
conservés dont data-cfasync), cache-busting via ?v={{ asset_version }}
(source unique A40), scripts externes = 'self' en CSP (pas de nonce requis)
- garde-fou : le script refuse tout bloc contenant {{ ou {%
- vérifs : node --check vert sur les 9, 0 script inline restant dans les
cibles, suite complète 1089/1089
Lint (la moitié « ajouter les templates à eslint » de l'audit) :
- eslint.config.mjs existait (flat v9, sans dépendances npm) mais AUCUN
binaire eslint n'était installé → npm i -g eslint
- `eslint static/js` → 0 erreur, 120 warnings (no-unused-vars 69,
no-empty 36, no-undef 15) sur 8 fichiers = baseline à nettoyer
- les extraits sont couverts d'office par la config (static/js/**/*.js)
Reste A27 : blocs interpolés Jinja (page_editor 2 517, local_workspace 2 031,
base 1 523, database_table 1 323, settings 1 093, realtime 531 ≈ 9 661 L)
→ extraction en 2 temps (config JSON injectée + script statique).
suite **1089/1089** · ruff 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
Rocket Loader (Cloudflare) reecrit les balises script inline en type=...-text/javascript et les execute de facon differee, ce qui casse l'enregistrement des composants Alpine et l'init des pages lors d'un acces direct via le tunnel (ex. /settings ne se chargeait pas au complet). Ajout de data-cfasync=false sur les scripts inline des pages completes et des partiels du shell (settings, accounts, trash, workspace, board, gitea_workspace, welcome, import, page_editor_collection/embed, _database_table_scripts, _notification_bell, public_page, local_workspace). Les fragments charges via HTMX restent inchanges.
loadSubdir() créait les icônes des enfants avec textContent au lieu de
innerHTML : le markup SVG (<svg width=...>) retourné par getSvgIcon() était
affiché comme texte brut quand on ouvrait un répertoire de la section Gitea.
Le rendu racine (loadSidebarTree) utilisait déjà innerHTML ; aligné.
- Add global window.FlowDeck namespace with createPage() and showCreateFolderModal()
- createPage() creates a Notion-style page via /board/api/pages (same as sidebar bottom-right button)
- showCreateFolderModal() opens a global themed modal (dark/light) instead of browser prompt()
- All New Page buttons now call window.FlowDeck.createPage()
- All New Folder buttons now call window.FlowDeck.showCreateFolderModal()
- Global New Folder modal uses existing .flowdeck-modal CSS classes for theme consistency
- Updated Ctrl+N keyboard shortcut to use unified createPage()
Backend:
- Added _nav_breadcrumb() to build page hierarchy chain (root → current)
- New /api/nav/menu endpoint returns workspace/folder children with has_children flag
- view_page() and view_page_root() now pass breadcrumb_items, nav_workspace_id, nav_page_id to template
Header template (_header.html):
- Replaced static breadcrumb with Alpine.js fdBreadcrumb() component
- Breadcrumb items now clickable with dropdown menus showing
Config:
- Fixed SQLite path parsing on Windows (handle drive letters without prepending /)
- Changed default theme from dark to light
- Updated OAuth test credentials (gitea_oauth_client_id/secret)
Auth:
- First real user (excluding default admin with no password) becomes admin
- Changed user count query to exclude users without password_hash
File viewer:
- Removed separate _render_file_viewer() —
Replaced global function giteaWorkspace() with Alpine.data('giteaWorkspace', ...)
registered via document.addEventListener('alpine:init', ...). This is the
recommended Alpine 3 approach that guarantees the component factory is
available before Alpine processes x-data directives.
Changed x-data="giteaWorkspace()" to x-data="giteaWorkspace" (Alpine.data
calls the factory automatically, no parentheses needed).
The <script> defining giteaWorkspace() was placed AFTER the <div x-data>
in the DOM, causing Alpine to try to evaluate giteaWorkspace() before
the function was defined. This resulted in 'Uncaught ReferenceError' for
all component properties (fileLoading, fileLanguage, showFile, etc.).
Fix: moved the <script> block to appear BEFORE the x-data div in the
HTML source, ensuring the function is defined when Alpine initializes.
The 'get sortedTreeItems()' getter caused Alpine's Proxy to fail during
component initialization, resulting in all properties being undefined
(fileLoading, fileLanguage, showFile, etc. — Uncaught ReferenceError).
Fix:
- Replaced getter with _sortTree() method + sortedTreeItems property
- _sortTree() called after treeItems is updated in loadMainTree()
- sortedTreeItems explicitly set to [] on error/empty paths
Backend:
- _sidebar_data() no longer clears workspace_pages for Gitea workspaces
The local tree stays intact in the 'Private' section
- workspace_pages is always loaded from DB (local files)
Sidebar template (base.html):
- Workspace section renamed to 'Private' when gitea_workspace is true
- New 'Repository (Gitea)' section added below, visible only for Gitea
workspaces, with its own toggle (sectionsOpen.gitea) and refresh button
- Uses #sidebar-gitea-items container for the remote file tree
Gitea workspace:
- loadSidebarTree() now targets #sidebar-gitea-items
- window._gwData exposed for sidebar refresh button
- sectionsOpen now includes gitea: true by default
Gitea workspace sidebar:
- loadSidebarTree() now uses innerHTML replacement instead of DOM
manipulation (avoids Alpine reactivity overwrites)
- Ensures workspace section is visible when tree loads
- Shows error message when Gitea API fails (no token/gateway)
- Better error handling with console.error catch
Header actions inline:
- page_editor: moved Edited/Share/Copy/Favorite/More buttons into
the unified header (right_actions), accessible via window.E
(editorState global reference set in init)
- Removed duplicate page-topbar div from content area
- Share dialog, More menu, and Activity popover stay in content
area (triggered by header buttons via window.E references)
- Gitea workspace: header now uses page-action-btn for consistency
Move to workspace:
- movePage() opens a dialog listing all workspaces via /api/workspaces
- doMove(wsId) calls PUT /api/pages/{id}/move with workspace_id
- Updated move API to accept JSON body with workspace_id for cross-workspace moves
Gitea workspace context menu:
- Right-click on any file/folder shows Rename + Delete options
- gwRename() prompts for new name, calls PUT /api/gitea/.../file
- gwDelete() confirms then calls DELETE /api/gitea/.../file
- Both trigger refreshTree() after success
Activity popover:
- 'Edited X ago' timestamp is now clickable (▾ indicator)
- Opens popover showing edited time + created date
- formatDate() helper for readable date formatting
- Main content area now shows file/folder tree with breadcrumb navigation
- Click folders to drill down, click files to open
- Breadcrumb shows current path with clickable segments
- Sorted tree: folders first, then files alphabetically
- refreshTree() now reloads without full page reload
- Empty state with 'New file' button when folder is empty
- loadGiteaTree: skip if giteaWorkspace component already loaded tree
- loadGiteaTree: check r.ok, add console.error logging, show error in UI
- loadGiteaTree: use .bind(this) for correct this in callbacks
- gitea_workspace.html: replace querySelector('[x-data]') with captured self
in loadSidebarTree, loadSubdir, Private Pages click handlers
- Prevents loadGiteaTree from overwriting loadSidebarTree results
on /gitea-workspace page
Erreur: block 'page_title' defined twice → TemplateAssertionError
Fix: title_prefix (nouveau bloc) pour <title>, page_title (existant) pour Alpine
10 templates mis à jour avec title_prefix.
- Le titre de page (onglet) : 'bruno/flowdeck' au lieu de 'Gitea Workspace'
- Le topbar breadcrumb : owner/repo correctement peuplé
- workspace_name + workspace_initial passés au template
Téléchargés et servis depuis /static/js/:
- alpine.min.js (44 KB) — remplace unpkg.com
- htmx.min.js (51 KB) — remplace unpkg.com
- sortable.min.js (45 KB) — remplace cdn.jsdelivr.net
- prism.min.js (19 KB) + prism.css (1.3 KB) — remplace cdnjs
Corrige le bug 'rien ne fonctionne' quand le réseau est lent/absent:
sans CDN, Alpine.js/HTMX/SortableJS ne chargeaient pas →
tous les @click, x-show, x-data, drag-drop inopérants.
- POST /api/gitea/projects/{owner}/{repo}/upload (binary upload)
- get_file_commits() in GiteaClient
- GET /api/gitea/projects/{owner}/{repo}/commits (per-file history)
- POST .../sync-labels (Gitea labels → FlowDeck tags)
- New file form + upload button in gitea_workspace.html topbar
1. _require_gitea: fallback token admin pour les GET (orgs, projects, tree, file)
_require_user_gitea: token OAuth requis pour les write (save, delete)
2. Private Pages: table gitea_private_pages (user_id, owner, repo, title, content)
API CRUD: GET/POST/PUT/DELETE /api/gitea/projects/{o}/{r}/private-pages
UI: liste + éditeur dans le workspace Gitea, lien 🔒 Private Pages dans sidebar
Lié logiquement au projet (owner+repo), conservé entre sessions
Avant: _sidebar_data() utilisait asyncio.run() → RuntimeWarning + 302 redirects
Après: gitea_workspace.html appelle loadSidebarTree() → fetch API → injecte dans #sidebar-workspace-items
- base.html: ajout id='sidebar-workspace-items' sur le <ul> workspace
- dashboard.py: _sidebar_data() ne fait plus de fetch serveur, juste passe owner/repo
1. Tab user repos: fetch username via /auth/user → affiche 'bruno' au lieu de 'Personal'
2. Sidebar: ws_cookie='gitea:owner:repo' → _sidebar_data() fetch le tree depuis Gitea API
3. base.html: data-gitea-path/type/sha sur les items workspace pour les projets Gitea
4. gitea_workspace.html: refonte sans tree panel → navigation via sidebar
- Clic fichier → ouvre dans le contenu (lecture + edit + commit)
- Clic dossier → lazy-load les enfants dans le sidebar
5. openGitea(): set cookie flowdeck_workspace avant navigation