diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py
index b0d73f7..789683b 100644
--- a/app/routers/dashboard.py
+++ b/app/routers/dashboard.py
@@ -1507,3 +1507,63 @@ async def update_account(request: Request):
response = JSONResponse({"status": "ok", "user": {k: user_data[k] for k in ("id","login","full_name","email","is_admin")}})
response.set_cookie("flowdeck_session", new_session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
+
+
+# ═══════════ Sidebar Refresh API ═══════════
+
+@router.get("/api/sidebar/workspace-tree")
+async def sidebar_workspace_tree(request: Request):
+ """Return the sidebar workspace tree as HTML fragment.
+
+ Called by appState().refreshSidebarTree() after CRUD operations
+ in the main content area to keep the sidebar in sync.
+ """
+ from jinja2 import Environment, FileSystemLoader
+ from app.routers.board import _load_workspace_pages
+
+ user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
+ if not user:
+ return HTMLResponse("")
+
+ ws_cookie = request.cookies.get("flowdeck_workspace", "")
+ if not ws_cookie:
+ return HTMLResponse('
')
+
+ # Gitea workspace — no server-side tree, loaded client-side
+ if ws_cookie.startswith("gitea:"):
+ return HTMLResponse('')
+
+ try:
+ ws_id = int(ws_cookie)
+ with get_conn() as conn:
+ # Verify workspace belongs to user
+ row = conn.execute(
+ "SELECT id FROM workspaces WHERE id=? AND owner_id=?",
+ (ws_id, user["id"])
+ ).fetchone()
+ if not row:
+ return HTMLResponse(
+ ''
+ )
+
+ pages = _load_workspace_pages(ws_cookie)
+ if not pages:
+ return HTMLResponse(
+ ''
+ )
+
+ # Render the tree using the same Jinja2 macro as base.html
+ env = Environment(loader=FileSystemLoader("app/templates"))
+ template = env.from_string(
+ "{% from 'base.html' import render_workspace_tree %}"
+ "{{ render_workspace_tree(pages) }}"
+ )
+ html = template.render(pages=pages)
+ return HTMLResponse(html)
+ except (ValueError, Exception):
+ return HTMLResponse(
+ ''
+ )
diff --git a/app/templates/base.html b/app/templates/base.html
index 46ff33a..5c1d9f1 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -636,11 +636,17 @@
case 'rename':
var newName = prompt('Rename:', name);
if (newName && newName.trim()) {
+ var self = this;
fetch('/api/local-workspace/items/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({ name: newName.trim() })
- }).then(function(r) { if (r.ok) window.location.reload(); });
+ }).then(function(r) {
+ if (r.ok) {
+ self.refreshSidebarTree();
+ document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh'));
+ }
+ });
}
break;
case 'delete':
@@ -970,7 +976,7 @@
body: JSON.stringify({ name: name.trim(), type: 'folder' })
})
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
- .then(() => { window.location.reload(); })
+ .then(() => { this.refreshSidebarTree(); })
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
},
deleteWorkspacePage(pageId) {
@@ -980,10 +986,25 @@
headers: { 'X-CSRF-Token': this.getCsrfToken() }
})
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
- .then(() => { window.location.reload(); })
+ .then(() => { this.refreshSidebarTree(); document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh')); })
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
},
+ refreshSidebarTree() {
+ // Reload the sidebar workspace tree after CRUD in the main content area
+ fetch('/api/sidebar/workspace-tree')
+ .then(r => r.text())
+ .then(html => {
+ const ul = document.getElementById('sidebar-workspace-items');
+ if (!ul) return;
+ // Preserve the "Manage Workspaces" link (last )
+ const manageLink = ul.querySelector('li:last-child');
+ ul.innerHTML = html;
+ if (manageLink) ul.appendChild(manageLink);
+ })
+ .catch(e => console.error('Sidebar refresh failed:', e));
+ },
+
// ── Drag & Drop ──
dragOverId: null,
dragStart(ev, id) {
@@ -1176,6 +1197,13 @@
}
};
+ // -- Global sidebar refresh handler (called by local_workspace after CRUD) --
+ document.addEventListener('flowdeck:sidebar-refresh', function() {
+ var appEl = document.querySelector('[x-data]');
+ var data = appEl && appEl.__x ? appEl.__x.$data : null;
+ if (data && data.refreshSidebarTree) data.refreshSidebarTree();
+ });
+
// ═══════════ GLOBAL KEYBOARD SHORTCUTS ═══════════
document.addEventListener('keydown', function(e) {
var tag = (document.activeElement && document.activeElement.tagName) || '';
diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html
index d16f023..47f57cd 100644
--- a/app/templates/local_workspace.html
+++ b/app/templates/local_workspace.html
@@ -1111,6 +1111,8 @@ window._wsInitData = (function() {
_reloadAfterAction() {
this._saveExpandedState();
this._loadFolder(this.currentFolder);
+ // Notify sidebar to refresh its tree
+ document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh'));
},
// ── Inline Rename ──
@@ -1555,6 +1557,8 @@ window._wsInitData = (function() {
this.doFilter();
}
this._saveExpandedState();
+ // Notify sidebar to refresh its tree
+ document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh'));
}
},