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
Avant: oauth_redirect_uri codé en dur à localhost:8080
→ impossible d'utiliser l'OAuth depuis une autre machine du réseau
Après: le redirect_uri est construit à partir du header Host de la requête
→ localhost:8080 → http://localhost:8080/auth/callback
→ 192.168.30.101:8080 → http://192.168.30.101:8080/auth/callback⚠️ L'utilisateur doit ajouter les 2 URIs dans Gitea OAuth App settings.
Avant: get_provider('gitea') → None (pas de client OAuth configuré)
→ fallback créait automatiquement une session admin
→ le bouton 'Register with Gitea' connectait l'utilisateur en admin !
Après: affiche une page d'erreur propre:
'⚠ Gitea OAuth not configured — The Gitea integration has not
been set up by the server administrator. ↩ Use local login'
1. Pas de Gitea sans config: _require_gitea() → 401 si pas de token OAuth
2. Register Gitea: boutons "Login/Register with Gitea" dynamiques selon l'onglet
3. Settings → Integrations: ✅ Active / 🔗 Connect / Disconnect + API status/disconnect
4. Workspaces: tabs par org (All | org1 | org2) + barre recherche 🔍 filtrage live
5. API: GET /api/gitea/status, DELETE /api/gitea/disconnect
GiteaClient:
- __init__(user_token=None) — per-user OAuth token ou admin token
- get_repo_contents(owner, repo, path) — lazy: un niveau à la fois
- get_file_content(owner, repo, path) — base64 décodé
- create_or_update_file(...) — PUT avec sha pour update
- delete_file(owner, repo, path, sha, message)
- _invalidate_tree_cache() — invalidation après write
Routes (/api/gitea):
- GET /orgs — liste des organisations
- GET /projects?org=x — repos user ou org
- GET /projects/{owner}/{repo}/tree?path=x — arborescence lazy
- GET /projects/{owner}/{repo}/file?path=x — contenu fichier
- PUT /projects/{owner}/{repo}/file — save + commit
- DELETE /projects/{owner}/{repo}/file?path=x&sha=x — delete
- GET /projects/{owner}/{repo}/labels — labels → tags
Helper: get_user_gitea_client(request) → GiteaClient ou None
DB:
- tags.user_id REFERENCES users(id)
- UNIQUE(name, user_id) au lieu de UNIQUE(name)
- Migration v2.6: alter + recreate table
API (tous les endpoints tags):
- POST/PUT/DELETE /api/settings/tags: filtré par user_id
- GET /api/settings/tags/all: retourne uniquement les tags du user
- GET/POST /api/local-workspace/*/tags: scope user
→ Les tags de l'utilisateur 1 ne sont pas visibles par l'utilisateur 2
Le cookie de session n'était pas mis à jour après modification du compte
→ le template re-rendait avec les anciennes données au rechargement.
Maintenant le serveur émet un nouveau cookie avec les données fraîches.
Backend:
- PUT /api/settings/account (full_name, login, email, password)
- Vérifie que le username n'est pas déjà pris
- Password min 6 caractères
Frontend (Settings → My Account):
- Champs éditables: Username, Full name, Email
- Section Change password avec toggle 👁/🙈
- Bouton Save changes + Update password
- Message de confirmation ✓ / ✗ avec timeout 3s
- Bouton 👁/🙈 dans le champ password (login + register)
- Label changé de 'Email' à 'Email or username' (les users créés via admin ont un login, pas forcément un email)
- Type email → type text pour accepter les usernames
1. ctxAddNewTag lit window._ctxMenuData.ctxNewTagColor (pas this.ctxNewTagColor)
→ la couleur sélectionnée dans le menu est maintenant sauvegardée
2. onContextMenu: charge workspaceTags si vide avant de calculer ctxAvailTags
→ les tags existants apparaissent dans le dropdown
3. Settings: CSS modal-overlay/modal-box/.btn-danger ajouté
→ le modal delete n'est plus transparent
4. Rename tag: modal avec input au lieu de prompt()
→ double-clic → modal avec couleur + input + autofocus
5. doFilter: _filterTreeByTag filtre displayTree quand tagFilter est actif
→ le filtrage par tags fonctionne dans toutes les vues
Context menu (_ctxMenuData):
- Remplacé getters par propriétés simples (Alpine ne réagit pas aux getters)
- Sync _wsInitData → _ctxMenuData dans onContextMenu, ctxAddNewTag, ctxRemoveTag
- Les 4 tags existants apparaissent maintenant dans le dropdown
Settings > Tags:
- Double-clic sur un tag → prompt rename (PUT /api/settings/tags/<id>)
- Suppression: modal de confirmation au lieu de confirm() natif
- Affiche le nombre d'items impactés si tag utilisé
_wsInitData (162 keys) causait "Cannot redefine property: $nextTick"
quand réutilisé comme x-data sur .ctx-menu. Alpine ajoute des propriétés
magiques ($nextTick, $el, etc.) et certaines entrent en conflit avec
les propriétés de l'objet massif.
Solution: window._ctxMenuData — objet minimal avec getters/setters
déléguant à _wsInitData. Seulement 20 propriétés, zéro conflit.
+ Restauré les directives Alpine (x-show, x-for, :style) dans le menu
puisque le scope Alpine fonctionne maintenant.
Le .ctx-menu était hors du scope _wsInitData dans le DOM (sibling du wrapper).
Ajouter x-data directement sur l'élément lui donne accès à toutes les variables
Alpine internes (ctxTagExistingOpen, ctxTagAdding, ctxAvailTags, ctxTagColors, etc.)
Les toggles de dropdown utilisent maintenant du DOM natif pour éviter les
problèmes de scope.
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)
Alpine ne détecte pas les changements sur ctxMenu (objet imbriqué
hors du scope _wsInitData dans le DOM). Le :style et x-show restent
bloqués sur les valeurs initiales.
Solution radicale: manipulation DOM directe
- onContextMenu: menu.style.display='block' + position left/top
- ctxMenuOpen*/Rename/Duplicate/Delete: menu.style.display='none'
- @click.outside/@keydown.escape: DOM direct aussi
- ctx-menu HTML: style="display:none" initial
x-show="ctxMenu.visible" ne réagit pas aux changements d'objet imbriqué
dans Alpine. :style avec display: ctxMenu.visible ? 'block' : 'none'
est bindé directement sur le style inline, donc réactif.
_wsInitData.onContextMenu() remplit _wsInitData.ctxMenu
→ ctxMenu = _wsInitData.ctxMenu recopie dans Alpine
→ Alpine détecte le changement et affiche le menu
Alpine ne détecte pas les changements sur les propriétés imbriquées
d'un objet (ctxMenu.visible = true). Il faut réassigner l'objet entier:
this.ctxMenu = {visible:true, x:..., y:..., node:..., tags:...}
Impact: onContextMenu (show), ctxMenuOpenPage/OpenFolder/Rename/Duplicate/Delete (hide)
+ repositionnement dans nextTick
Le Alpine proxy ne contient pas les méthodes (onContextMenu, onTouchStart, etc.)
car l'objet _wsInitData est mergé avec appState() et les fonctions ne sont
pas copiées comme propriétés directes.
Fix: _wsInitData.onContextMenu.call(, )
→ = Alpine data réactive → this.ctxMenu.visible = true fonctionne
Desktop:
- @contextmenu.prevent corrigé: window._wsData → onContextMenu()
(window._wsData n'existait pas, le handler était mort)
Mobile:
- @touchstart/@touchmove/@touchend ajoutés sur ws-split
- Détection long-press: >600ms sans mouvement >10px
- Recherche du [data-ws-id] le plus proche
- Appel onContextMenu avec les coordonnées tactiles
Fermeture:
- @click.outside ajouté sur .ctx-menu
- @click.self maintenu sur ws-split pour fermer en cliquant ailleurs
Le > de fermeture était sur la même ligne que <div class="ws-split">
→ les attributs (@contextmenu, @click, tabindex, x-ref) sur les lignes
suivantes devenaient du contenu texte affiché dans la page.
Fix: > déplacé à la fin de x-ref="layout">
Les modals (create/rename/delete) étaient des siblings de ws-split dans le DOM,
pas des enfants — à cause d'un problème de nesting HTML avec les <template> Alpine.
Résultat: x-show="showCreate" ne trouvait pas 'showCreate' dans le scope parent.
Fix: wrapper <div x-data="_wsInitData"> autour de tout le {% block content %},
plus déplacement des @dragover/@dragleave/@drop/@keydown sur ce wrapper.
Le ws-split garde ses attributs restants (@contextmenu, @click, tabindex, x-ref).
- Ajouté texte 'New File' et 'New Folder' aux boutons icônes
- Ajouté CSS .btn-create avec width:auto pour accommoder le texte
(l'ancien CSS .btn-icon imposait width:34px fixe qui coupait le texte)
- Les boutons étaient des icônes seules (📄📁) sans texte,
donc difficiles à trouver pour l'utilisateur
Alpine évalue les expressions comme tree, flatTree, online, etc.
dans un scope qui n'hérite pas toujours de _wsInitData.
En les exposant comme window.X, Alpine les trouve toujours.
Supprime les 36 erreurs restantes sur /local-workspace.
- Changé x-data="window._wsInitData" → x-data="_wsInitData"
(Alpine évalue mieux sans le préfixe global)
- Supprimé <template x-for="node in flatTree"> dans une table cachée
(x-show="false", jamais visible mais Alpine l'évaluait quand même)
Cause: l'IIFE window._wsInitData était située APRÈS le <div ws-split>.
Alpine évaluait x-data="window._wsInitData" AVANT que l'IIFE ne tourne
→ toutes les propriétés (flatTree, tree, online, ctxMenu, preview*, etc.)
étaient undefined → 148 erreurs Alpine Expression Error.
Fix: déplacement de l'IIFE (lignes 871-2405) avant le ws-split (ligne 339).
Ordre corrigé: IIFE → _wsInitData prêt → ws-split x-data OK.
Vérification: le diagnostic log « ws-split about to render, _wsInitData exists: »
affichera maintenant true au lieu de false.
- 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
Alpine 3.14.9 ne supporte pas les getters dans le Proxy x-data.
Converti en propriétés mises à jour via _recompute() dans:
- init() après chargement tree
- _loadFolder() après chaque navigation
- doSort() / doFilter() après modifs
Getters remplacés:
- pathValue, canGoBack, canGoForward, flatTree,
filteredTableItems, contentSnippetMap
- _computeFilteredTableItems() extrait de get filteredTableItems
Zéro getter restant, braces/parens équilibrés, 73 tests OK
Cause racine: local_workspace.html override le block topbar avec son
propre hamburger button qui avait encore ; au lieu de , dans l'expression
ternaire. Alpine parse les expressions via return <expr> → le ; terminait
le return prématurément → SyntaxError → cascade d'échecs d'initialisation
des composants enfants.
Corrigé dans:
- base.html: sidebar overlay @click (1 occurrence)
- local_workspace.html topbar hamburger (1 occurrence)
- local_workspace.html: 10 autres expressions (viewMode, searchQuery,
filterType, draggedItemId, tagFilter, ctxAddNewTag, navigateToPath)
73 tests OK
Le hamburger button avait mobileSidebarOpen = true; sidebarCollapsed = false
Le ; casse le parser d'expression Alpine (return <expr> → le ; termine le statement)
En cascade, l'erreur empêche l'initialisation correcte du composant parent,
ce qui brise tous les composants enfants (wsInit → ctxMenu, modals, preview, etc.)
C'est le bug qui persistait depuis des jours à travers 5+ approches différentes.
- 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