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
Avant: doCreate() appelait _reloadAfterAction() → page reload → le
nouvel élément ne s'affichait pas immédiatement dans l'arbre.
Après: insertion AJAX du nouveau node dans this.tree via
_insertIntoParent(). Le parent est auto-expandé. displayTree mis à jour
si aucun filtre actif. Plus besoin de refresh.
Reste reload pour rename/delete/upload (complexité renderChildren).
Avant: tout window.location.reload() réinitialisait expanded={},
causant le collapse de tous les dossiers après un drop.
Fix:
- expanded initialisé depuis localStorage('fd_ws_expanded')
- _saveExpandedState() sauve avant toute navigation/reload
- _reloadAfterAction() = save + reload
- toggleExpand, navigateToFolder, expandAll sauvent l'état
- Tous les reload (create, rename, delete, move, upload) passent par _reloadAfterAction()
Sidebar workspace:
- Arbre plus compact: padding réduit (4+12*depth au lieu de 8+16),
gap 4px, min-height 24px, font 13px, chevron 12px
- Boutons d'action (📄📁✏️🗑) remplacés par menu contextuel
au clic droit (@contextmenu.prevent) et long-press mobile (500ms)
- Menu: New File, New Folder (si dossier), Rename, Delete
- Nom de l'item affiché en bas du menu
Page workspace:
- Boutons ⊞ (Expand All) et ⊟ (Collapse All) dans la toolbar
- expandAll(): parcourt l'arbre, set expanded[id]=true, toggle DOM
- collapseAll(): vide expanded, masque tous les <ul>
Ajout de _extractPreview(): parse le JSON Notion blocks, extrait le
texte des champs .content ou .text de chaque block. Fallback sur le
raw content si le format n'est pas blocks/markdown.
Avant: {"blocks":[{"type":"text","content":"Hello"}]}
Après: Hello
Refactor complet de renderChildren:
- window._wsData exposé dans init() (au lieu de document.querySelector)
- data-ws-action + data-ws-id + data-ws-name sur les boutons d'action
- Event delegation: handleTreeAction() sur le <ul> via @click Alpine
- Suppression de event.stopPropagation() qui bloquait la délégation
- Chevron: onclick utilise window._wsData, data-ws-id ajouté
- Drag & drop + hover: utilisent window._wsData
Toutes les actions fonctionnent maintenant à n'importe quel niveau de profondeur.
Remplacé li.querySelector(':scope > ul.ws-tree') par une boucle sur
itemDiv.parentElement.children. Évite les problèmes de compatibilité
de :scope et trouve le <ul> frère du .ws-tree-item de manière fiable.
Chemin: btn → .ws-tree-item → parentElement (<li>) → itère children
→ premier UL.ws-tree → toggle display.
Cause: renderChildren génère du HTML statique (x-html). Le chevron
appelait toggleExpand(id) qui mettait à jour this.expanded[id] mais
le <ul> des enfants n'avait pas de x-show → le DOM ne réagissait pas.
Fix: toggleExpand(id, btn) accepte maintenant l'élément bouton.
Pour les items renderChildren, il:
- toggle la classe CSS 'open' sur le chevron (rotation 90°)
- trouve le <ul class=ws-tree> frère dans le <li> parent
- bascule son style.display entre '' et 'none'
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>).
Les nodes de l'API tree ont 'id', mais startDelete/doRename/startRename
utilisaient 'db_id'. Pour les items x-for, db_id était undefined →
les appels API pointaient vers /items/undefined.
Fix: addDepths() ajoute db_id = id sur chaque node, alignant le
format x-for avec celui de renderChildren.
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.
Root cause: sectionsOpen did not include workspace:true by default,
and toggle state was not persisted. On every page navigation,
sectionsOpen.workspace defaulted to undefined (falsy), hiding the
workspace tree section.
Fixes:
- sectionsOpen now defaults with workspace:true
- sectionsOpen persisted to localStorage on toggle and before navigation
- beforeunload handler auto-saves expandedFolders + sectionsOpen
(covers <a href> navigation for file clicks too)
- navigateToFolder saves both expandedFolders and sectionsOpen
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)
Alpine processes DOM top-to-bottom. x-data='wsInit()' was evaluated
before the script defining wsInit() was parsed. Moved Alpine.data
registration to a script tag BEFORE the x-data div. Also removed
duplicate script block at the bottom.
The template had x-data="{inline object}" with x-init="init()" but the
inline object had no init method. wsInit() function was defined but
never referenced. Changed x-data to wsInit() which returns the full
component with init() — Alpine auto-calls init() on mount.
Complete rewrite of local_workspace.html with:
- flatten() as standalone function (not in Alpine component)
- wsInit() uses x-init for async loading
- No optional chaining or template literals (broader compat)
- :key uses array index to avoid undefined keys
- Simple var declarations throughout
- All modals unified with x-show toggles
- tree starts as [] (not undefined) so x-for doesn't crash
- :key uses node.db_id || 'item-'+idx for uniqueness
- _loaded guard prevents rendering before async init completes
- Empty state shows while loading, tree only renders after load()
The tree API returns {type: 'folder'|'page'} but flattenTree() was
accessing node.is_folder which is undefined for all items. All items
got is_folder: undefined → no children → but more critically, the
conditionals in the template also broke. Fixed to check node.type.
Both sidebar and workspace page DnD handlers now use @drop.prevent
to stop browser's default text insertion behavior. Without .prevent,
the browser tries to navigate to the dragged text as a URL, breaking
the drop event.
Added x-cloak directive and [x-cloak] CSS so button never flashes
before Alpine loads. Button is in base.html so appears on all pages
that extend the base template.
When sidebarCollapsed=true, a hamburger button appears at top-left
corner to restore the sidebar. CSS fixed positioning outside sidebar
so it's always visible even when sidebar is collapsed.
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.
Server-side rendered sidebar doesn't auto-update after AJAX creation.
Changed create modal to reload page after success so sidebar picks up
new pages from workspace_pages template variable.
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)
CSRF middleware now excludes /auth/register, /auth/local-login,
/api/user, and /api/workspace paths. Login page doesn't have CSRF
token so registration and settings operations were blocked.
- CSS media queries: <=1024px (tablet) and <=768px (mobile)
- Sidebar hidden on mobile, togglable via .mobile-open
- Topbar, page editor, workspace, settings adapt padding
- Share dialog shrinks to 90vw on mobile
- Get Started toolbar wraps and centers pills
- No JS changes needed — pure CSS responsive
- 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.