fix: hamburger menu sur toutes les pages + cookies path="/" Chrome fix
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- 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)
This commit is contained in:
2026-07-11 00:30:59 -04:00
parent e2a739c4c6
commit c5398757ca
7 changed files with 41 additions and 94 deletions
+1
View File
@@ -33,6 +33,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
httponly=False, # Must be readable by JS
samesite="lax",
max_age=86400,
path="/",
)
return response
+4 -4
View File
@@ -100,7 +100,7 @@ async def login(request: Request, provider: str = Query("gitea")):
user_data = dict(user)
session = SessionManager.create_session(user_data)
response = RedirectResponse(url="/workspaces", status_code=302)
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
state = secrets.token_hex(32)
@@ -146,7 +146,7 @@ async def register(request: Request):
session = SessionManager.create_session(user_data)
from fastapi.responses import JSONResponse
response = JSONResponse({"status": "ok", "user": {"login": email, "name": name}})
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
@@ -200,7 +200,7 @@ async def local_login(request: Request):
conn.commit()
session = SessionManager.create_session(ud)
response = JSONResponse({"status": "ok", "user": {"login": ud["login"], "name": ud["full_name"]}})
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
@@ -266,7 +266,7 @@ async def callback(
# Create session
session = SessionManager.create_session(user_data)
response = RedirectResponse(url="/workspaces", status_code=302)
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
+19 -89
View File
@@ -4,18 +4,24 @@
{% block topbar %}
<header class="topbar">
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Workspace</span>
<span class="page-title-topbar">{{ workspace_name }}</span>
</div>
<div class="topbar-right"></div>
<div class="topbar-right">
<a href="/workspaces" class="page-action-btn" title="All workspaces">🏠</a>
</div>
</header>
{% endblock %}
{% block content %}
<style>
.ws-layout{max-width:800px;margin:0 auto;padding:40px 24px;}
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;}
@media (max-width: 768px) { .ws-layout { padding: 20px 12px; } }
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px;}
.ws-header h1{font-size:24px;}
.ws-toolbar{display:flex;gap:8px;}
@@ -55,79 +61,6 @@
.delete-confirm{padding:12px;background:rgba(255,80,80,.08);border-radius:8px;margin-top:12px;font-size:13px;color:var(--text-secondary);}
</style>
<script>
document.addEventListener('alpine:init', function() {
Alpine.data('wsInit', function() {
return {
tree:[],
loaded:false,
showCreate:false, showRename:false, showDelete:false,
createType:'file', newName:'', parentFolder:null, target:null,
async init() {
try {
var r = await fetch('/api/local-workspace/tree');
var d = await r.json();
this.tree = flatten(d.tree||[],0);
this.loaded = true;
} catch(e) { this.loaded = true; }
},
openModal(type, parent) {
this.createType = type; this.parentFolder = parent||null; this.newName = '';
this.showCreate = true;
var self = this;
this.$nextTick(function() {
var el = self.$refs.cinp;
if (el) setTimeout(function() { el.focus(); }, 100);
});
},
async doCreate() {
var n = this.newName.trim(); if (!n) return;
var body = { name:n, type:this.createType };
if (this.parentFolder) body.parent_id = this.parentFolder.db_id;
var r = await fetch('/api/local-workspace/items', {
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
});
if (r.ok) {
this.showCreate = false; this.parentFolder = null; this.newName = '';
window.location.reload();
}
},
openPage(id) { window.location = '/pages/'+id; },
startRename(node) {
this.target = node; this.newName = node.name.trim(); this.showRename = true;
var self = this;
this.$nextTick(function() {
var el = self.$refs.rinp;
if (el) setTimeout(function() { el.focus(); }, 100);
});
},
async doRename() {
var n = this.newName.trim(); if (!n||!this.target) return;
await fetch('/api/local-workspace/items/'+this.target.db_id, {
method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:n})
});
this.showRename = false; this.target = null; this.newName = '';
await this.init();
},
startDelete(node) { this.target = node; this.showDelete = true; },
async doDelete() {
if (!this.target) return;
await fetch('/api/local-workspace/items/'+this.target.db_id, {method:'DELETE'});
this.showDelete = false; this.target = null;
await this.init();
}
};});
});
</script>
<div class="ws-layout" x-data="wsInit()">
<div class="ws-header">
@@ -214,6 +147,17 @@ document.addEventListener('alpine:init', function() {
</div>
<script>
function flatten(nodes, depth) {
var result = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
var isFolder = n.type==='folder';
result.push({db_id:n.id, name:n.name, icon:isFolder?'📁':'📄', is_folder:isFolder, depth:depth});
if (n.children) result = result.concat(flatten(n.children, depth+1));
}
return result;
}
document.addEventListener('alpine:init', function() {
Alpine.data('wsInit', function() {
return {
@@ -263,9 +207,6 @@ document.addEventListener('alpine:init', function() {
this.parentFolder = null;
this.newName = '';
window.location.reload();
} else {
var e = await r.json();
alert(e.error||'Failed');
}
},
@@ -307,16 +248,5 @@ document.addEventListener('alpine:init', function() {
}
};});
});
function flatten(nodes, depth) {
var result = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
var isFolder = n.type==='folder';
result.push({db_id:n.id, name:n.name, icon:isFolder?'📁':'📄', is_folder:isFolder, depth:depth});
if (n.children) result = result.concat(flatten(n.children, depth+1));
}
return result;
}
</script>
{% endblock %}
+8 -1
View File
@@ -1,6 +1,13 @@
{% extends "base.html" %} {% block page_icon %}📄{% endblock %} {% block
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
<!-- Page editor uses its own topbar -->
<header class="topbar" style="background:var(--bg-primary);">
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5);font-size:13px">{{ page.title }}</span>
</div>
</header>
{% endblock %} {% block content %}
<div class="page-editor-wrapper" x-data="editorState()">
<!-- ═══════════ Top Action Bar ═══════════ -->
+3
View File
@@ -4,6 +4,9 @@
{% block topbar %}
<header class="topbar">
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">
Settings
+3
View File
@@ -4,6 +4,9 @@
{% block topbar %}
<header class="topbar">
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Workspace</span>
<span class="page-title-topbar">Projects</span>
+3
View File
@@ -4,6 +4,9 @@
{% block topbar %}
<header class="topbar">
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Home</span>
<span class="page-title-topbar">Workspaces</span>