fix: avatars persistant — route fichier + couleur en DB + sidebar affichage
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Route manquante GET /api/settings/avatar/{filename} → sert le fichier uploadé
  (résout le 404 sur l'image uploadée)
- Colonne avatar_color ajoutée à users (migration ALTER TABLE try/except)
- set_avatar_color sauvegarde la couleur dans la DB (pas seulement local)
- _sidebar_data() lit avatar_url + avatar_color depuis la DB
- Sidebar avatar: fond coloré quand pas d'image, image quand uploadée
  avec background-image:url() + initiale en fallback
This commit is contained in:
2026-07-12 16:38:21 -04:00
parent 4668eb77ed
commit 14ad34c886
3 changed files with 35 additions and 4 deletions
+6
View File
@@ -27,6 +27,7 @@ def init_db():
full_name TEXT NOT NULL DEFAULT '', full_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '',
avatar_url TEXT NOT NULL DEFAULT '', avatar_url TEXT NOT NULL DEFAULT '',
avatar_color TEXT NOT NULL DEFAULT '#3A3A3A',
password_hash TEXT, password_hash TEXT,
is_admin INTEGER NOT NULL DEFAULT 0, is_admin INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1, is_active INTEGER NOT NULL DEFAULT 1,
@@ -316,6 +317,11 @@ def init_db():
conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)") conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)")
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
# Migration: add avatar_color (v2.3.0+)
try:
conn.execute("ALTER TABLE users ADD COLUMN avatar_color TEXT NOT NULL DEFAULT '#3A3A3A'")
except sqlite3.OperationalError:
pass
# Migration: add sort_order for drag & drop tree reordering (v1.1.0+) # Migration: add sort_order for drag & drop tree reordering (v1.1.0+)
try: try:
conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0") conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0")
+28 -3
View File
@@ -36,6 +36,19 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
ws = user.get("login", "Bruno") if user else "Bruno" ws = user.get("login", "Bruno") if user else "Bruno"
initial = ws[0].upper() if ws else "B" initial = ws[0].upper() if ws else "B"
# Get avatar info from DB
avatar_url = ""
avatar_color = "#3A3A3A"
if user:
try:
with get_conn() as conn:
row = conn.execute("SELECT avatar_url, avatar_color FROM users WHERE id = ?", (user["id"],)).fetchone()
if row:
avatar_url = row["avatar_url"] or ""
avatar_color = row["avatar_color"] or "#3A3A3A"
except Exception:
pass
recent_pages = [] recent_pages = []
for repo in repos[:10]: for repo in repos[:10]:
full_name = repo.get("full_name", "") full_name = repo.get("full_name", "")
@@ -76,6 +89,8 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
"favorite_pages": [], "favorite_pages": [],
"shared_pages": [], "shared_pages": [],
"user": user, "user": user,
"avatar_url": avatar_url,
"avatar_color": avatar_color,
"workspace_key": "", "workspace_key": "",
} }
@@ -1125,6 +1140,17 @@ async def upload_avatar(request: Request):
return {"avatar_url": avatar_url} return {"avatar_url": avatar_url}
@router.get("/api/settings/avatar/{filename:path}")
async def serve_avatar_file(filename: str):
"""Serve an uploaded avatar image file."""
from fastapi.responses import FileResponse
from pathlib import Path
filepath = Path("/data/avatars") / filename
if not filepath.is_file():
return JSONResponse({"error": "Not found"}, status_code=404)
return FileResponse(filepath)
@router.get("/api/avatar/{user_id:int}") @router.get("/api/avatar/{user_id:int}")
async def get_avatar(user_id: int): async def get_avatar(user_id: int):
"""Redirect to the user's avatar.""" """Redirect to the user's avatar."""
@@ -1137,15 +1163,14 @@ async def get_avatar(user_id: int):
@router.post("/api/settings/avatar-color") @router.post("/api/settings/avatar-color")
async def set_avatar_color(request: Request): async def set_avatar_color(request: Request):
"""Set the user's avatar background color (for initial-based avatars).""" """Set the user's avatar background color."""
import json
body = await request.json() body = await request.json()
color = body.get("color", "#3A3A3A") color = body.get("color", "#3A3A3A")
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user: if not user:
return {"error": "Not authenticated"}, 401 return {"error": "Not authenticated"}, 401
with get_conn() as conn: with get_conn() as conn:
conn.execute("UPDATE users SET avatar_url = '' WHERE id = ?", (user["id"],)) conn.execute("UPDATE users SET avatar_url = '', avatar_color = ? WHERE id = ?", (color, user["id"]))
conn.commit() conn.commit()
return {"status": "ok", "color": color} return {"status": "ok", "color": color}
+1 -1
View File
@@ -65,7 +65,7 @@
<div class="sidebar-workspace-header" @click="workspaceMenuOpen = !workspaceMenuOpen" @mouseenter="wsHeaderHover=true" @mouseleave="wsHeaderHover=false" <div class="sidebar-workspace-header" @click="workspaceMenuOpen = !workspaceMenuOpen" @mouseenter="wsHeaderHover=true" @mouseleave="wsHeaderHover=false"
:class="{ hover: wsHeaderHover || workspaceMenuOpen }"> :class="{ hover: wsHeaderHover || workspaceMenuOpen }">
<div class="workspace-info"> <div class="workspace-info">
<div class="workspace-avatar">{{ workspaceInitial }}</div> <div class="workspace-avatar" style="{% if avatar_url %}background-image:url('{{ avatar_url }}');background-size:cover;{% else %}background:{{ avatar_color }};{% endif %}">{% if not avatar_url %}{{ user.full_name[:1].upper() if user and user.get('full_name') else workspaceInitial }}{% endif %}</div>
<span class="workspace-name">{{ user.full_name or user.login or workspaceName }}</span> <span class="workspace-name">{{ user.full_name or user.login or workspaceName }}</span>
</div> </div>
<span class="workspace-chevron" :class="{ open: workspaceMenuOpen }">▾</span> <span class="workspace-chevron" :class="{ open: workspaceMenuOpen }">▾</span>