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(); })">
- - ▾ + ▾ +
+ +
+
+
{{ workspaceInitial }}
+
+
{{ user.full_name or user.login }}
+
{{ user.email or '' }}
+
+
+
+ ⚙️ Settings + 🔄 Switch workspace +
+ 🚪 Log out
@@ -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 '' }}
-
-
-
-
Name
-
Display name
-
-
- - -
-
-
-
-
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
-
- -
-