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. 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.
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
- 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
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.
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.
Replicates Notion's page layout from reference images:
- Top bar: Private badge, Share button, Link copy, Star/favorite toggle, ⋮ menu
- Share modal: Publish toggle, Copy link, Invite people with permission select
- Empty page toolbar: Get started with H1/H2/H3/bullet/todo/callout/quote
- Star toggle integrated with favorites API (POST/DELETE /board/api/favorites)
- board.py view_page now passes workspace context + page_favorited
- dashboard.py view_page_root also passes page_favorited
- CSS: .page-topbar, .share-modal, .empty-page-toolbar, toggle switch, more menu
Bug: lib_private filtered on p.source == '🔒 Private' which only matched pages
without a workspace. All pages have parent_section='Private' by default, so
filtering by source excluded them all.
Fix: add 'section' field to page dict from parent_section DB column,
filter lib_private by p.section == 'Private'
6 files changed:
- db.py: migrate favorites table FK from collection_pages(id) to pages(id)
- board.py: add favorites API (POST/DELETE /board/api/favorites/{id}, GET list)
- board.py: _sidebar_data() now loads favorite_pages from DB via JOIN
- dashboard.py: library_page loads lib_favorites from DB (not parent_section)
- csrf.py: exclude /board/api/favorites from CSRF checks
- base.html: context menu toggles Add/Remove Favorites based on state
- base.html: favoriteIds Alpine set initialized from server-rendered favorites
- test_app.py: test_favorites_crud rewritten for new page-based favorites API
Favorites now work end-to-end:
- Right-click → Add to Favorites (or Remove if already favorited)
- Sidebar Favorites section shows favorited pages
- Library Favorites tab shows the same pages
- API: POST/DELETE /board/api/favorites/{page_id}, GET /board/api/favorites
- Sidebar data (board._sidebar_data()) no longer overwritten by library content
- New variables: lib_recent, lib_favorites, lib_shared, lib_private
- Pages categorized by parent_section column: Private/Favorites/Shared
- Recents tab shows all non-trashed pages ordered by updated_at
- library.html updated to use lib_* variables instead of sidebar data
- Favorites/Shared tabs hidden when empty (lib_has_favorites/lib_has_shared)
3 bugs fixed:
1. dashboard.py /library was registered before board.router and had no workspace context → removed old endpoint, re-added with owner/repo params using board._sidebar_data()
2. base.html navigateTo('/library') lost workspace → now auto-appends ?owner=X&repo=Y from workspaceKey
3. openLibrary(id) used bare window.location.href → now uses this.navigateTo()
Now the Library page:
- Shows pages filtered by workspace when coming from a project
- Preserves the sidebar context (no more Admin Dashboard fallback)
- Tabs (Recents, Favorites, Shared, Private) display project pages from DB
- Ajout des 3 nouvelles tables pour le Database Concept
- collections: base de données abstraite, indépendante de Gitea
- collection_pages: pages génériques avec parent_id et property_values_json
- collection_views: vues configurables par collection
- Mise à jour test_db_tables_exist pour inclure les nouvelles tables
- 28/28 tests passent
- Add sort_order column to pages table for drag & drop reordering
- Rewrite _sidebar_data with recursive _build_page_tree for nested hierarchy
- Add tree toggle buttons (▶ collapsed / ▼ expanded) on parent items
- Alpine.js expandedNodes state with toggleTreeNode() for expand/collapse
- Jinja2 recursive macro render_tree_item for proper nesting + indentation
- SortableJS integration on .sidebar-items for drag & drop repositioning
- New PUT /board/api/pages/{id}/move API for persisting moves
- Auto-set sort_order on page creation (max+1 for same parent)
- CSS: .tree-toggle, .tree-toggle-placeholder, grab cursor on page-icon
- 28/28 tests pass, all endpoints healthy
- Page /accounts: profil utilisateur éditable, comptes connectés, workspace members
- API GET/PUT /api/users/me: lecture/écriture profil local (full_name, email)
- Users list from local DB, auth via Gitea OAuth2
- Imports SessionManager clean dans api.py
- pages.workspace = owner/repo (ex: bruno/flowdeck) au lieu du nom user
- Sidebar: Kanban board + 4 sous-vues avec URLs distinctes (?view=kanban|table|...)
- Cliquer sur 'Table view' dans sidebar → change la vue à droite
- Dashboard (/): sidebar montre uniquement les projets Gitea
- Board (/board/{o}/{r}): sidebar montre les pages de ce projet
- Changement de projet = changement de contexte (pages isolées)
- newPage/newSubPage passent workspace_key à l'API
- Auto-switch view depuis paramètre URL dans board.html
- Ajouté /pages/{page_id} dans dashboard.py (route racine, pas /board/pages/...)
- La redirection après création de page fonctionne maintenant
- Les pages Markdown s'affichent correctement
- dashboard.py: decode session cookie and pass user to template
- notes.py: pass user to template (both GET and POST)
- CSS: .header-right flex layout, #user-info styles, min-height instead of fixed height
- Removed duplicate #user-info CSS block
- Login now properly shows admin/Deconnexion after authentication
Gitea's /api/v1/user/repos already includes all org repos the user
has access to. Fetching org repos separately added duplicates.
Removed the redundant org-repo loop from dashboard and /api/projects.
Also fixed template to use repo.owner.login instead of _org hack.