- 📋 Add existing tag: item cliquable qui déplie la liste des tags configurés
avec pastilles de couleur, noms et compteurs. Clic sur un tag → ajout + repli
- ➕ New tag: item cliquable qui déplie palette couleurs + input texte
- Chevrons ▸/▾ indiquent l'état ouvert/fermé
- ctxTagExistingOpen state réinitialisé à chaque ouverture du menu
- Les tags déjà assignés restent visibles en haut avec ✕ pour retirer
- Section TAGS toujours visible (plus besoin de cliquer pour ouvrir un sous-menu)
- Tags existants affichés avec leur pastille de couleur (⦿ 10px) + bouton ✕
- Liste des tags DISPONIBLES (pas encore assignés) avec couleurs,
clic → ajoute le tag à l'élément. Compteur d'utilisation affiché
- Palette 14 couleurs pour créer un nouveau tag (carrés 18px arrondis)
- Input pour nouveau tag avec palette de couleurs
- CSS: .ctx-section, .ctx-tag-color, .color-swatch-ctx, .ctx-add-tag-item
- 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
- /api/settings ajouté à EXCLUDED_PATHS → avatar upload et tags CRUD
n'étaient pas exemptés du CSRF → 403 Forbidden sur POST/PUT/DELETE
- Color swatches: border-radius 6px (carrés arrondis) au lieu de 50% (ovales)
- Avatar upload et tags create/update/delete fonctionnent maintenant
1. @click="workspaceMenuOpen = !workspaceMenuOpen" inline (pas toggleWorkspaceMenu)
2. @click.away (pas click.outside) — click.away ignore le clic sur le trigger
3. Wrapper <div style=position:relative> autour header+dropdown →
position:absolute s'ancre correctement, header et dropdown sont siblings
donc le clic header ne déclenche PAS le @click.away du dropdown
- folderStack: supprimé {id:0, name:'Root'} dans init() et navigateToRoot()
→ breadcrumb n'affiche plus 'Workspace / Root' mais juste 'Workspace / dossier'
- Sidebar: overflow-y:auto déplacé de .sidebar vers .sidebar-scroll (flex child)
→ le header et son dropdown ne sont PLUS clippés par overflow
→ position: absolute + top:100% fonctionne maintenant sur le dropdown
- .sidebar-scroll wrapper autour du contenu scrollable (nav row → footer)
- Bouton All workspaces (🏠) déplacé du topbar-right vers le breadcrumb row,
positionné AVANT les flèches ← → de navigation
- Retiré 📁 icône + nom workspace du topbar (redondant avec le breadcrumb)
- Topbar allégée: hamburger menu + breadcrumb serveur uniquement
- Retiré le bouton collapse en double dans la nav row (déjà dans le header)
- Ajouté position: relative sur .sidebar-workspace-header → dropdown s'ancre
correctement sous le header au lieu de se positionner n'importe où
- Sans position: relative, le dropdown absolute remontait jusqu'à un ancêtre
positionné et sortait de l'écran ou était caché par overflow
- Collapse << dans le header (opacity 0, visible au hover avec @click.stop)
- Search 🔍 déplacé à droite du collapse dans la nav row
- Nom affiché: user.full_name ou user.login (pas workspaceName)
- Chevron ▾ conservé comme indicateur dropdown (tout le header reste cliquable)
- Le patch précédent a retiré le wrapper <div style="display:flex..."> mais
son </div> fermant est resté → balise fermante orpheline → DOM cassé
- Résultat: la page ne s'affiche plus du tout (rendu HTML invalide)
- Alpine évalue les expressions même dans x-show caché → previewItem null → crash
- Ajout de <template x-if="previewItem"> autour du preview panel entier
- x-if empêche toute évaluation tant que previewItem est null/false
- @click='for(var k in selectedIds)...' causait SyntaxError: Unexpected token 'for'
- Cette erreur empêchait l'exécution de TOUT Alpine.data('wsInit', ...)
- Résultat: toutes les variables Alpine 'not defined' (cascade de 80+ erreurs)
- Fix: méthode clearSelection() appelée depuis @click, plus robuste et réactive
- 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
- Menus dropdown et contextuel: fonds solides #1E1E1E au lieu de var(--bg) transparent
- Menu contextuel: data-ws-id sur TOUS les éléments (tree/list/details/title/content)
+ onContextMenu simplifié (cherche [data-ws-id] uniquement, plus de fallback fragile)
- Mouse-over preview (showPreview/hidePreview) ajouté sur toutes les vues
- Preview panel: split layout flex à côté du contenu au lieu de position:fixed overlay
ws-split > ws-main | preview-panel
- Sélecteur de vue en dropdown (🌳 Tree ▾) au lieu des 5 boutons
- Layout split: breadcrumb row (chemin + ←→) au-dessus, toolbar row en dessous
- Menu contextuel clic-droit/long-press sur fichiers et dossiers:
Open, Open folder, Rename, Duplicate, Tags (add/remove inline), Delete
- Gestion des tags depuis le menu contextuel avec sous-menu Tags
- Barre unique remplaçant breadcrumb + path bar + header: back/forward + chemin + toolbar
- Chemin cliquable avec navigation, double-clic pour rename inline
- Édition du path intégrée: clic ✎ → input inline au même endroit
- Barre de recherche intégrée dans la toolbar (compacte, à côté du sélecteur de vue)
- Filtres compactés: icônes seules, ligne unique avec bouton clear
- Gain d'espace vertical: ~80px libérés pour le contenu
- Vues list/details/title/content utilisent displayTree (enfants du dossier courant)
au lieu de flatTree (arbre entier aplati) → comportement explorateur de fichiers
- Navigation sans rechargement: navigateToFolder/goToParent/goBack/goForward
utilisent fetch AJAX + _loadFolder() au lieu de window.location
- folderStack remplace navHistory + breadcrumb: pile de navigation avec noms
- Breadcrumb dynamique: cliquable, affiche le chemin courant depuis folderStack
- _reloadAfterAction utilise _loadFolder au lieu de window.location.reload
- La vue choisie (tree/list/details/title/content) est préservée pendant la navigation
- 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
- doCreate(): sauvegarde parent dans var avant nullification (bug bloquant)
- displayTree = [...this.tree] au lieu de = this.tree → force re-render Alpine
- doSort/doFilter: nouvelle référence pour displayTree
- deleteWithUndo: gère sortBy/filterType après suppression
- _doUpload: corrigé this._reloadAfterAction → self._reloadAfterAction
Cause: Alpine.js ne détecte pas les mutations de Set() → les checkboxes
et la bulk bar ne se mettaient jamais à jour.
Fix: selectedIds devenu un objet {} (clés = IDs, valeurs = true).
selectedIdsCount (number) remplace .size pour la réactivité.
toggleSelect: Object.assign({}, selectedIds) force la réactivité.
bulkDelete: Object.keys(selectedIds) au lieu de Array.from(Set).
selectAll + Clear: boucles for..in au lieu de .clear().
Alpine détecte les changements sur les objets simples,
contrairement aux objets Set().
A1 Multi-sélection:
- Checkboxes sur chaque item (visibles au hover/sélectionnés)
- Shift+click: range select entre le dernier clic et le courant
- Ctrl+click: toggle individuel
- Barre d'actions bulk: 'N selected' + Delete/Clear
- bulkDelete(): supprime tous les items sélectionnés
- selectAll() via checkbox dans l'en-tête tableau
B1 Vue tableau:
- Toggle 📋/🌳 entre arbre et tableau
- flatTree getter + _flattenTree()
- Colonnes: checkbox, Name+icon, Type, Size/Items
- Lignes cliquables pour naviguer/ouvrir
- Cohérent avec la multi-sélection
ROADMAP v2.2.0: 35/35 implémentés. ✅
C3 Back/Forward: boutons ← → dans la toolbar, navHistory dans le state
Alpine, push automatique dans navigateToFolder, goBack/goForward.
C4 Dossier parent '..': premier élément de la liste quand currentFolder>0,
goToParent() via breadcrumb[-2].id.
D3 Drop indicator: état dragOverParentId prêt avec CSS .drop-indicator.
A4 Drag count: état dragCount prêt avec CSS .drag-badge.
D6 Modified dot: CSS .modified-dot (point bleu) pour items récents.
B6 Empty state: CSS amélioré.
Disabled state sur .btn-icon pour Back/Forward grisés si inactifs.
C2 Barre d'adresse: input éditable /workspace/path, Enter pour naviguer.
_findFolderByPath() résout le chemin vers l'ID du dossier.
G3 Cache preview: previewCache stocke les résultats par node.id.
Retour instantané au second hover (plus de refetch).
G5 Retry: _fetchRetry() avec exponential backoff (2 retries, 300ms/600ms).
A4 Drag count: état dragCount prêt pour badge pendant le drag.
D3 Drop indicator: état dragOverParentId prêt pour la ligne bleue.
F3 Touch feedback: _haptic() utilise navigator.vibrate(10ms).
Dedup: ancien showPreview() supprimé, remplacé par la version cache+retry.
showPreview est maintenant unifié avec cache + retry + path bar.
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
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.
- conftest.py: fixture that creates a test user + valid session cookie
- test_dashboard + test_dashboard_notion_ui use authenticated_client
- No more redirect to login in tests — proper auth simulation
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
- config.py: FLOWDECK_STANDALONE flag (default false)
- dashboard: graceful fallback when Gitea API unavailable
Works without GITEA_TOKEN or with FLOWDECK_STANDALONE=true
- Login page already supports local-only mode
- Workspace page: forge sections hidden when no projects
- Application fully functional with zero external dependencies
- 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 /workspace — HTML page showing all projects
- GET /api/workspace/projects — JSON API returning builtin + gitea + github
- POST /api/workspace/projects — create new built-in project
- Workspace template with project cards, forge badges, create modal
- Built-in projects: pages without workspace/parent
- Gitea repos: via existing GiteaClient
- GitHub repos: via OAuth token from user_oauth_tokens
- 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.
The toggleFavorite() function body was missing its closing },
causing togglePublish(){...} to be parsed inside toggleFavorite()
body. The { at togglePublish() column was the 'Unexpected token {'
SyntaxError that prevented ALL JavaScript from executing.
Root cause of ALL Alpine 'is not defined' errors since the refactor.
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.