diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py
index 12065d7..c5bd1bc 100644
--- a/app/routers/dashboard.py
+++ b/app/routers/dashboard.py
@@ -1080,6 +1080,80 @@ async def select_workspace(request: Request, ws_id: int):
return response
+# ═══════════ Settings Page ═══════════
+
+@router.get("/settings", response_class=HTMLResponse)
+async def app_settings_page(request: Request):
+ """Settings & configuration page."""
+ from jinja2 import Environment, FileSystemLoader
+ env = Environment(loader=FileSystemLoader("app/templates"))
+ sidebar = _sidebar_data(request, [])
+ user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
+ if not user:
+ return RedirectResponse("/auth/login?provider=local", status_code=302)
+ ctx = {**sidebar, "user": user}
+ template = env.get_template("settings.html")
+ return template.render(**ctx)
+
+
+# ═══════════ Tag Management API (global) ═══════════
+
+@router.post("/api/settings/tags")
+async def create_tag_global(request: Request):
+ """Create a tag with optional color."""
+ import json
+ body = await request.json()
+ tag_name = body.get("name", "").strip().lower()
+ color = body.get("color", "#787774")
+ if not tag_name:
+ return {"error": "Tag name required"}, 400
+ with get_conn() as conn:
+ tag = conn.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)).fetchone()
+ if tag:
+ conn.execute("UPDATE tags SET color = ? WHERE id = ?", (color, tag["id"]))
+ conn.commit()
+ return {"tag": {"id": tag["id"], "name": tag_name, "color": color}}
+ cursor = conn.execute("INSERT INTO tags (name, color) VALUES (?, ?)", (tag_name, color))
+ conn.commit()
+ return {"tag": {"id": cursor.lastrowid, "name": tag_name, "color": color}}
+
+
+@router.put("/api/settings/tags/{tag_id:int}")
+async def update_tag_global(tag_id: int, request: Request):
+ """Update a tag (name or color)."""
+ import json
+ body = await request.json()
+ with get_conn() as conn:
+ if "name" in body:
+ conn.execute("UPDATE tags SET name = ? WHERE id = ?", (body["name"].strip().lower(), tag_id))
+ if "color" in body:
+ conn.execute("UPDATE tags SET color = ? WHERE id = ?", (body["color"], tag_id))
+ conn.commit()
+ return {"status": "ok"}
+
+
+@router.delete("/api/settings/tags/{tag_id:int}")
+async def delete_tag_global(tag_id: int):
+ """Delete a tag and its page associations."""
+ with get_conn() as conn:
+ conn.execute("DELETE FROM page_tags WHERE tag_id = ?", (tag_id,))
+ conn.execute("DELETE FROM tags WHERE id = ?", (tag_id,))
+ conn.commit()
+ return {"status": "ok"}
+
+
+@router.get("/api/settings/tags/all")
+async def list_all_tags_global():
+ """List all tags with counts."""
+ with get_conn() as conn:
+ rows = conn.execute(
+ "SELECT t.id, t.name, t.color, COUNT(pt.page_id) as count "
+ "FROM tags t LEFT JOIN page_tags pt ON pt.tag_id = t.id "
+ "GROUP BY t.id ORDER BY t.name"
+ ).fetchall()
+ return {"tags": [dict(r) for r in rows]}
+
+
# ═══════════ Tags API ═══════════
@router.get("/api/local-workspace/tags")
diff --git a/app/templates/base.html b/app/templates/base.html
index 0760a6e..d17bca5 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -61,7 +61,7 @@
x-data x-init="$nextTick(() => { if(typeof initTreeSortable==='function') initTreeSortable(); })">
-
+
+
@@ -599,13 +614,9 @@
try { localStorage.setItem('fd_sections', JSON.stringify(this.sectionsOpen)); }
catch(e) {}
},
+ workspaceMenuOpen: false,
toggleWorkspaceMenu() {
- if (window.innerWidth <= 768) {
- this.mobileSidebarOpen = !this.mobileSidebarOpen;
- this.sidebarCollapsed = !this.mobileSidebarOpen;
- } else {
- this.sidebarCollapsed = !this.sidebarCollapsed;
- }
+ this.workspaceMenuOpen = !this.workspaceMenuOpen;
},
navigateTo(url) {
this.mobileSidebarOpen = false;
diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html
index 4a1bd9b..d7579eb 100644
--- a/app/templates/local_workspace.html
+++ b/app/templates/local_workspace.html
@@ -1155,19 +1155,49 @@ document.addEventListener('alpine:init', function() {
this.previewPanelContent = ''
+ '
📁'
+ '
' + (node.child_count||0) + ' item(s)
'
- + '
'
+ + '
'
+ '
';
return;
}
try {
+ var name = (node.name || '').toLowerCase();
+ // Image preview
+ if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(name)) {
+ var wsId = 1; // from workspace
+ var imgUrl = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
+ this.previewPanelContent = ''
+ + '

'
+ + '
';
+ return;
+ }
+ // PDF preview
+ if (/\.pdf$/.test(name)) {
+ this.previewPanelContent = ''
+ + '
📕'
+ + '
' + this._escHtml(node.name) + '
'
+ + '
' + (node.size_display || '') + '
'
+ + '
Open in viewer'
+ + '
';
+ return;
+ }
var r = await this._fetchRetry('/api/local-workspace/page-content/' + node.id, true);
var content = r.content || '';
var format = r.format || '';
var preview = '';
if (format === 'file') {
+ // Try to detect image/pdf from content metadata
+ try {
+ var meta = JSON.parse(content);
+ if (meta.file_path && /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(meta.file_path)) {
+ var wsId2 = 1;
+ var imgUrl2 = '/api/files/' + wsId2 + '/' + encodeURIComponent(meta.file_path);
+ this.previewPanelContent = '
';
+ return;
+ }
+ } catch(e) {}
preview = ''
+ '📄'
- + 'Uploaded file — open to view
';
+ + (node.size_display || '') + ' — open to view';
} else if (format === 'blocks') {
try {
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
diff --git a/app/templates/settings.html b/app/templates/settings.html
index 0354eb1..93b931a 100644
--- a/app/templates/settings.html
+++ b/app/templates/settings.html
@@ -1,5 +1,5 @@
{% extends "base.html" %}
-{% block page_title %}Account Settings{% endblock %}
+{% block page_title %}Settings{% endblock %}
{% block page_icon %}⚙️{% endblock %}
{% block topbar %}
@@ -8,185 +8,144 @@
-
- Settings
-
- Account Settings
+ ⚙️ Settings
-
{% endblock %}
{% block content %}
-
-
-
+
+
-
Profile
-
-
-
-
Email
-
Used for login and notifications
-
-
{{ user.email or user.login or '' }}
-
-
-
-
-
Password
-
Change your password
-
-
-
-
-
-
-
-
-
-
-
-
Connected Forges
-
-
-
-
🔗 Gitea
-
Connect to your Gitea instance
-
- {% if gitea_connected %}
-
✅ Connected
-
- {% else %}
-
- {% endif %}
-
-
-
-
🐙 GitHub
-
Connect to GitHub
-
- {% if github_connected %}
-
✅ Connected
-
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
API Tokens
-
-
-
-
Generate API Token
-
For programmatic access to FlowDeck API
-
-
-
-
-
-
-
-
Copy this token now — it won't be shown again
+
🏷️ Tags Management
+
Manage all tags across your workspace. Changes apply globally.
+
+
+
-
+
-
Active Sessions
-
-
-
-
Current session
-
You are logged in
-
-
+
📋 Workspace
+
+
+
Workspace name
+
Current: {{ workspaceName }}
-
{% endblock %}
diff --git a/static/css/app.css b/static/css/app.css
index 00867d4..2939a51 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -162,6 +162,56 @@ input, select, textarea { font-family: inherit; color: inherit; }
color: var(--text-primary);
}
+.workspace-chevron {
+ font-size: 10px;
+ color: var(--text-dim);
+ transition: transform 0.2s ease;
+}
+.workspace-chevron.open {
+ transform: rotate(180deg);
+}
+
+/* User menu dropdown */
+.user-menu-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 8px;
+ right: 8px;
+ margin-top: 4px;
+ background: #1E1E1E;
+ border: 1px solid var(--border-strong);
+ border-radius: 10px;
+ box-shadow: 0 12px 32px rgba(0,0,0,.6);
+ z-index: 400;
+ padding: 4px 0;
+ overflow: hidden;
+}
+.um-user-info {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+}
+.um-avatar {
+ width: 28px; height: 28px;
+ background: var(--bg-tertiary);
+ border-radius: 6px;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 14px; font-weight: 600; color: var(--text);
+ flex-shrink: 0;
+}
+.um-name { font-size: 14px; font-weight: 600; color: var(--text); }
+.um-plan { font-size: 12px; color: var(--text-dim); margin-top: 1px; }
+.um-sep { height: 1px; background: var(--border); margin: 3px 0; }
+.um-item {
+ display: flex; align-items: center; gap: 8px;
+ padding: 7px 14px; font-size: 13px; color: var(--text);
+ text-decoration: none; cursor: pointer; transition: background 100ms;
+}
+.um-item:hover { background: var(--bg-hover); }
+.um-danger { color: var(--danger); }
+.um-danger:hover { background: rgba(255,80,80,.1); }
+
.sidebar-actions {
display: flex;
align-items: center;