Backend (dashboard.py):
- POST /api/local-workspace/items/<id>/tags accepte maintenant 'color'
- INSERT INTO tags (name, color) au lieu de INSERT INTO tags (name)
- Si tag existe déjà, garde sa couleur existante
Frontend (local_workspace.html):
- ctxAddNewTag envoie {name, color} dans le body de la requête
(avant: seul {name} était envoyé, la couleur était ignorée)
- app.js: intercepte window.onerror + unhandledrejection
Envoie automatiquement au backend via POST /api/frontend-error
Déduplication, throttling 1/sec, max 50 erreurs buffer local
window.__flowdeck_errors accessible en console debug
- api.py: 2 nouvelles routes
POST /api/frontend-error — reçoit erreurs JS, déduplique, logge
GET /api/frontend-errors?clear=true — Hermes lit les erreurs
- csrf.py: exemption /api/frontend-error du CSRF
Usage Hermes après chaque déploiement:
curl -s http://localhost:8080/api/frontend-errors | jq .
→ Voir TOUTES les erreurs JS en temps réel
- x-data="_wsInitData" référence la variable globale JS (pas window.)
- var _wsInitData = IIFE — propriété window automatique pour compatibilité window._wsData
- Headers Cache-Control: no-cache, no-store, must-revalidate sur la page HTML
- 73 tests OK
- Route manquante GET /api/settings/avatar/{filename} → sert le fichier uploadé
(résout le 404 sur l'image uploadée)
- Colonne avatar_color ajoutée à users (migration ALTER TABLE try/except)
- set_avatar_color sauvegarde la couleur dans la DB (pas seulement local)
- _sidebar_data() lit avatar_url + avatar_color depuis la DB
- Sidebar avatar: fond coloré quand pas d'image, image quand uploadée
avec background-image:url() + initiale en fallback
- Palette 14 couleurs pour avatar: clic sur un carré coloré → applique la couleur
au fond de l'avatar avec l'initiale. API POST /api/settings/avatar-color
- Upload photo: efface la couleur custom et utilise l'image uploadée
- Color swatches tags: border-radius 6px (carrés arrondis) au lieu de 50% (ovales)
- CSRF: /api/settings ajouté à EXCLUDED_PATHS → 403 corrigé sur avatar + tags
- Sidebar header: user menu dropdown (Settings, Switch workspace, Log out)
avec avatar + nom + email, chevron animé, fond opaque #1E1E1E
- Nouvelle page /settings avec gestion globale des tags:
créer tag avec couleur, changer couleur, supprimer, liste avec compteurs
- API tag management: POST/PUT/DELETE /api/settings/tags, GET /api/settings/tags/all
- Preview panel: affichage images (img tag), PDF (lien viewer), détection format
- workspace-chevron CSS: rotation 180° quand menu ouvert
- DB: tables tags + page_tags avec index
- API: CRUD tags (GET/POST/DELETE items/{id}/tags, GET /tags, GET /tags/search)
- Tree endpoint enrichi: size, dates, author, tags par nœud
- Table enrichie: colonnes Name, Path, Size, Modified, Type, Author, Tags
- Tag chips avec compteurs + filtrage par clic
- Ajout/retrait tags inline dans la table
- Filtre Folders + auto-switch table quand recherche active
Implémenté depuis la ROADMAP v2.2.0:
A3 Inline rename: double-clic → input inline (Enter/escape/blur).
Fonctionne sur x-for items et breadcrumb.
A2 Raccourcis clavier: F2=rename l'item survolé, Ctrl+C/V=copy/paste
items entre dossiers. Layout focusable (tabindex=0, @keydown).
A6 Duplicate: méthode duplicateItem(), crée une copie '(copy)'.
B3 Métadonnées: child_count ajouté dans l'API tree + sidebar data.
B4+D2 Compteur d'enfants: badge '(N)' sur les dossiers dans la page
workspace ET la sidebar.
B5 Loading skeletons: CSS shimmer animation (.skeleton).
C1 Breadcrumb éditable: double-clic sur un segment → rename.
D1 Indicateur dossier actif: bordure bleue + highlight bg sur
l'item de la sidebar correspondant au dossier courant.
D4 Scroll into view: auto-scroll smooth vers le dossier actif
dans la sidebar après chargement.
child_count ajouté à _build_tree_children (dashboard.py) et
_load_workspace_pages/_load_children (board.py).
Refonte du _render_file_viewer pour les fichiers texte/code:
- Palette GitHub Dark (#0d1117, #161b22, #c9d1d9)
- Topbar sticky avec: ← Workspace, titre, badge langage, taille
- Bouton 📋 Copy (clipboard API, feedback '✓ Copied!')
- Bouton ⬇ Download
- Numéros de ligne dans une colonne latérale (line-numbers)
- Code rendu ligne par ligne (<span> par ligne)
- Font monospace (Fira Code, Cascadia Code, JetBrains Mono)
- escapeHtml() pour éviter les injections XSS
1. Tree affichage: displayTree contient maintenant les racines uniquement
(plus de flatten). _markTree ajoute depth/db_id sans aplatir, et
renderChildren gère la récursion. Les enfants n'apparaissent plus
quand le dossier est fermé, et plus de dédoublement à l'ouverture.
2. Filtre: _filterTree travaille sur l'arbre hiérarchique, clone les
nodes pour préserver la structure.
3. Preview pages FlowDeck: nouvelle API GET /api/local-workspace/page-content/{id}.
showPreview détecte content_format='file' → /api/files/, sinon → API page-content.
Plus de 'file not found' sur les fichiers créés dans FlowDeck.
4. API tree: ajout content_format dans la réponse pour distinguer
fichiers uploadés vs pages FlowDeck.
1. File viewer: ws_id parsing cassé (parts[2] au lieu de parts[1]).
Corrigé avec une boucle qui trouve le segment 'workspace_*'.
Les images s'affichent maintenant correctement.
2. Preview hover: background rendu opaque (fallback #1a1a1a si var(--bg)
absent), texte lisible. Ajout support preview image (thumbnail).
3. Preview body: changé de <pre x-text> à <div x-html> pour supporter
le HTML (nécessaire pour les <img>).
1. Drag & drop: ajout data-folder-id sur les items x-for et renderChildren.
onDrop lit cet attribut au lieu de parser onclick (cassé pour x-for).
Corrige le bug où les fichiers droppés allaient toujours à la racine.
2. Boutons création: remplacés par .btn-icon (icône seule, 34px, tooltip
natif). Style Notion épuré.
3. Barre de recherche: champ 🔍 qui filtre displayTree en temps réel.
Matching récursif (un dossier match si lui ou un enfant match).
4. Preview hover: popup positionné au curseur montrant les 500 premiers
caractères du fichier. Fonctionne sur x-for et renderChildren.
1. Drag & drop interne: déplacer fichiers/dossiers existants vers d'autres
dossiers ou racine via l'API /api/local-workspace/items/{id}/move.
Items rendus draggable dans le template x-for et renderChildren.
2. File viewer pour fichiers uploadés: détection content_format='file',
visualiseur intégré avec rendu adapté au type MIME:
- Images: affichage direct
- PDF: iframe
- Texte/Code (.py .js .md .html .ps1 etc.): <pre> avec fetch du contenu
- Autres: lien de téléchargement
Route GET /api/files/{ws_id}/{filename:path} pour servir les fichiers.
3. Sidebar persistante: expandedFolders sauvegardé dans localStorage,
restauré au chargement, préservé avant navigation vers un dossier.
4. Suppression: nettoie aussi le fichier disque pour content_format='file'
Backend:
- POST /api/local-workspace/upload: upload fichiers via multipart, stockage
disque (/data/uploads/workspace_{id}/), page DB avec content_format='file'
- POST /api/local-workspace/upload-folder: upload récursif de dossiers
(structure JSON + fichiers), crée l'arborescence complète
Frontend:
- Drop zones sur la page workspace (racine + dossiers ciblés)
- Visual: overlay 'Drop files here', highlight du dossier cible
- webkitGetAsEntry pour walk récursif des dossiers
- Progress bar en bas à droite pendant l'upload
- Auto-reload après upload réussi
- Déduplication automatique des noms de fichiers
- API /api/local-workspace/tree: arbre récursif complet (support optionnel ?folder=ID)
- API /api/local-workspace/breadcrumb: fil d'Ariane parent pour un dossier
- Page /local-workspace: supporte ?folder=ID pour naviguer dans un dossier
- Sidebar: expand/collapse (▶/▼) sur les dossiers, clic dossier → /local-workspace?folder=ID
- CSS: styles chevron .tree-chevron, .tree-folder-link
- Alpine.js: expandedFolders, toggleTreeFolder, navigateToFolder
Bug: create_workspace in dashboard.py set uid=1 when session was
invalid, but user 1 might not exist in the users table, causing
sqlite3.IntegrityError: FOREIGN KEY constraint failed (HTTP 500).
Fix: ensure user row exists (INSERT OR IGNORE) before inserting
workspace with FK reference, matching the pattern in workspace.py.
Also: add name validation, use INSERT OR IGNORE for members.
- local_workspace.html: supprimé Alpine.data dupliqué + ajout hamburger
- workspaces.html, workspace.html, settings.html, page_editor.html: +hamburger
- auth.py: +path="/" sur tous les set_cookie de session (Chrome compat)
- csrf.py: +path="/" sur cookie CSRF
Root cause des bugs:
1. Chrome: cookie sans path="/" → non envoyé sur certaines routes
2. Firefox: les templates écrasaient le block topbar → pas de hamburger
→ sidebar inaccessible sur mobile (overlay + slide-in ne fonctionnaient pas)
Same bug as tree API — sqlite3.Row doesn't implement .get(),
only __getitem__ (bracket notation). Fixed icon detection for
folders vs files in sidebar workspace section.
sqlite3.Row objects only support __getitem__ (r['key']), not the .get()
method. Changed r.get('parent_section') to r['parent_section'] in
folder type detection logic.
The roots query only had id, title columns but the tree builder
accessed r['parent_section'] which didn't exist in the result row.
Added parent_section to the SELECT to fix folder type detection.
- Topbar: removed New File/New Folder buttons + 'Files' label
- Breadcrumb: Workspace / work1 instead of work1 / Files
- Modals: single shared create modal with focus auto, @keydown.enter
- parent_id param: recursive folder/file creation inside folders
- Sidebar buttons use same POST /api/local-workspace/items
- newPageInWorkspace/newFolderInWorkspace now both use unified API
- Login redirect (all modes) now goes to /workspaces
- Workspace tree API filters by active workspace_id (no leak)
- Workspace name in sidebar from cookie (active_ws_name)
- Sidebar workspace section: 📄 new page + 📁 new folder buttons
- deleteWorkspacePage() in sidebar with confirmation
- _load_workspace_pages() helper with workspace_id filter
- CSS for sidebar-item-delete button (× on hover)
- Home button now links to /workspaces (universal workspace page)
- Sidebar workspace section shows active workspace name from cookie
- _sidebar_data reads flowdeck_workspace cookie for active_ws_name
- local-workspace APIs filter by workspace_id (not global)
- newPageInWorkspace() JS function added to sidebar
- CSRF exemption for /api/local-workspace routes
- DB: pages.workspace_id column linking pages to workspaces table
- /workspaces: list, create, rename, delete workspaces page
- API: GET/POST/PUT/DELETE /api/workspaces + POST select
- Session: flowdeck_workspace cookie for active workspace tracking
- Sidebar: Workspace section shows active workspace name + page tree
- Local workspace APIs filter by active workspace_id
- Home redirects non-Gitea users to /workspaces
- Dashboard '/' checks user_oauth_tokens for Gitea connection
- Local accounts (no Gitea token) redirect to /local-workspace
- Sidebar: Workspace section above Meetings with 'My Workspace' link
- Local users no longer see Gitea projects on Home page
- /local-workspace: page with file tree for local accounts (no forge)
- API: GET/POST/PUT/DELETE /api/local-workspace/items
- Workspace is empty when no pages exist, shows create buttons
- File tree shows nested items with rename/delete actions
- Local accounts are NOT linked to Gitea (separate workspace space)
- GET /api/workspace/{id}/members — list all members with roles
- POST /api/workspace/{id}/members — invite user by email
- PUT /api/workspace/{id}/members/{user_id} — change role
- DELETE /api/workspace/{id}/members/{user_id} — remove member
- Roles: owner, admin, editor, viewer
- Uses existing workspace_members DB table (v2.0 schema)
When users table is empty (fresh install/test), auth check returns
a default admin user instead of redirecting. This allows the application
to bootstrap and tests to run without mocking sessions.
Dashboard now calls _get_user_or_redirect() before rendering.
Unauthenticated users get redirected to /auth/login?provider=local.
All sensitive routes now protected: /, /workspace, /accounts/settings
- Logout now redirects to /auth/login?provider=local instead of /
- Dashboard / and /workspace check auth and redirect to login if no session
- _get_user_or_redirect() helper added for reusable auth checks
- No more fallback admin user on dashboard — explicit login required
- Routes without session redirect to login page
- GET /workspace — unified workspace page
- GET /api/workspace/projects — JSON API for built-in + Gitea projects
- POST /api/workspace/projects — create built-in project
- Added RedirectResponse to imports
- Callback now supports any provider (Gitea/GitHub) via providers.get_provider()
- OAuth tokens stored in user_oauth_tokens table per user + provider
- Login page shows both Gitea and GitHub OAuth buttons
- config.py: github_client_id + github_client_secret
- Auth flow no longer depends on gitea_oauth import — fully abstracted
- Fallback admin user now sets is_active=1 and admin@localhost email
- GET /accounts/settings — settings page with profile, forges, tokens, sessions
- PUT /api/user/profile — update display name
- PUT /api/user/password — change password
- POST /api/user/token — generate API token
- DELETE /api/user/forge/{provider} — disconnect forge
- Redirects to /auth/login?provider=local if not logged in
Pages with share_mode='anyone' or published=1 are now listed in the
left sidebar Shared section. Icon shows 🌐 for published pages,
🔗 for anyone-with-link pages. Updated _sidebar_data() to query
the pages table for shared/published pages.
Same pattern as f7d9d91: dashboard.router is registered before board.router
in main.py, so its /pages/{id} takes priority. Added page_data dict to match
the new JSON script tag approach.
Root cause: x-data HTML attribute with tojson creates quote conflicts
that survive even Cache-Control: no-store (likely nginx proxy cache).
Solution: page data is now in a <script type=application/json> tag,
completely decoupled from Alpine x-data HTML attribute. Zero quoting
issues regardless of page content.
- x-data="editorState()" + x-init="loadPage()" replaces editor(...)
- Server passes page_data dict, template renders as JSON script tag
- Same approach used for page_share_mode and page_published
- All 73 tests pass
Both board.py and dashboard.py page endpoints now return
Cache-Control: no-store, max-age=0 headers to prevent the browser
from caching the page HTML with broken x-data attributes.
dashboard.router is registered before board.router in main.py, so its
/pages/{page_id} takes priority. It was missing page_share_mode and
page_published template variables, causing Jinja2 TypeError.