Both board.py and dashboard.py page endpoints now return Cache-Control: no-store, max-age=0 headers to prevent the browser from caching the page HTML with broken x-data attributes.
217 lines
8.6 KiB
Python
217 lines
8.6 KiB
Python
"""FlowDeck — Dashboard: liste des projets Gitea + sidebar data."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request, Query
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from app.services.gitea_client import gitea
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["dashboard"])
|
|
|
|
|
|
def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
ws = user.get("login", "Bruno") if user else "Bruno"
|
|
initial = ws[0].upper() if ws else "B"
|
|
|
|
recent_pages = []
|
|
for repo in repos[:10]:
|
|
full_name = repo.get("full_name", "")
|
|
recent_pages.append({
|
|
"id": full_name,
|
|
"name": repo.get("name", full_name),
|
|
"icon": "📁",
|
|
"url": f"/board/{full_name}",
|
|
"active": False,
|
|
"indent": 0,
|
|
"depth": 0,
|
|
"has_children": False,
|
|
"children": [],
|
|
})
|
|
|
|
return {
|
|
"workspace_name": ws, "workspace_initial": initial,
|
|
"current_page": "Dashboard", "last_edited": "now",
|
|
"recent_pages": recent_pages,
|
|
"private_pages": [],
|
|
"favorite_pages": [],
|
|
"user": user,
|
|
"workspace_key": "",
|
|
}
|
|
|
|
|
|
@router.get("/trash", response_class=HTMLResponse)
|
|
async def trash_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
|
|
"""Trash page — scoped to workspace if owner/repo provided."""
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from app.routers.board import _sidebar_data as board_sidebar
|
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
|
sidebar = board_sidebar(request, owner, repo)
|
|
with get_conn() as conn:
|
|
ws_key = f"{owner}/{repo}" if owner and repo else ""
|
|
# Pages are soft-deleted via parent_section='Trash'
|
|
if ws_key:
|
|
rows = conn.execute(
|
|
"SELECT id, title, workspace FROM pages WHERE parent_section='Trash' AND workspace=? ORDER BY updated_at DESC",
|
|
(ws_key,),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"SELECT id, title, workspace FROM pages WHERE parent_section='Trash' ORDER BY updated_at DESC",
|
|
).fetchall()
|
|
sidebar["trash_items"] = [{"id": r["id"], "name": r["title"] or "Untitled", "workspace": r["workspace"]} for r in rows]
|
|
template = env.get_template("trash.html")
|
|
return template.render(**sidebar)
|
|
|
|
|
|
@router.get("/library", response_class=HTMLResponse)
|
|
async def library_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
|
|
"""Library page avec onglets Recents, Favorites, Shared, Private — scopé au workspace.
|
|
|
|
La sidebar garde exactement la même arborescence que dans le workspace.
|
|
Seul le contenu principal change pour afficher les pages triées par catégorie.
|
|
"""
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from app.routers.board import _sidebar_data as board_sidebar
|
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
|
sidebar = board_sidebar(request, owner, repo) # Sidebar intacte, inchangée
|
|
|
|
ws_key = f"{owner}/{repo}" if owner and repo else ""
|
|
with get_conn() as conn:
|
|
base_query = (
|
|
"SELECT id, title, workspace, parent_section, updated_at FROM pages "
|
|
"WHERE parent_section != 'Trash'"
|
|
)
|
|
params = []
|
|
if ws_key:
|
|
base_query += " AND workspace=?"
|
|
params.append(ws_key)
|
|
base_query += " ORDER BY updated_at DESC"
|
|
rows = conn.execute(base_query, params).fetchall()
|
|
|
|
def to_page(r):
|
|
r = dict(r)
|
|
return {
|
|
"id": f"page/{r['id']}",
|
|
"name": r["title"] or "Untitled",
|
|
"icon": "📄",
|
|
"url": f"/pages/{r['id']}",
|
|
"created_by": "You",
|
|
"source": r.get("workspace") or "🔒 Private",
|
|
"last_edited": r.get("updated_at", "now"),
|
|
"last_visited": r.get("updated_at", "now"),
|
|
"section": r.get("parent_section", "Private"),
|
|
}
|
|
|
|
all_pages = [to_page(r) for r in rows]
|
|
|
|
# Load favorites from DB for Library
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
uid = user["id"] if user and user.get("id") else 1
|
|
with get_conn() as conn:
|
|
fav_rows = conn.execute(
|
|
"SELECT p.id, p.title, p.workspace, p.updated_at FROM favorites f "
|
|
"JOIN pages p ON p.id = f.page_id "
|
|
"WHERE f.user_id=? ORDER BY f.position", (uid,)
|
|
).fetchall()
|
|
lib_favs = []
|
|
for r in fav_rows:
|
|
r = dict(r)
|
|
lib_favs.append({
|
|
"id": f"page/{r['id']}",
|
|
"name": r["title"] or "Untitled",
|
|
"icon": "📄",
|
|
"url": f"/pages/{r['id']}",
|
|
"created_by": "You",
|
|
"source": r.get("workspace") or "🔒 Private",
|
|
"last_edited": r.get("updated_at", "now"),
|
|
"last_visited": r.get("updated_at", "now"),
|
|
})
|
|
|
|
# Catégoriser par parent_section — sidebar data intacte
|
|
sidebar["lib_recent"] = all_pages
|
|
sidebar["lib_favorites"] = lib_favs
|
|
sidebar["lib_shared"] = []
|
|
sidebar["lib_private"] = [p for p in all_pages if p.get("section") == "Private"]
|
|
sidebar["lib_has_favorites"] = len(lib_favs) > 0
|
|
sidebar["lib_has_shared"] = False
|
|
|
|
template = env.get_template("library.html")
|
|
return template.render(**sidebar)
|
|
|
|
|
|
@router.get("/pages/{page_id}", response_class=HTMLResponse)
|
|
async def view_page_root(request: Request, page_id: int):
|
|
"""Render a Markdown page at root level with workspace context."""
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from app.routers.board import _sidebar_data as board_sidebar
|
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
|
with get_conn() as conn:
|
|
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
|
|
if not row:
|
|
return HTMLResponse("<h2>Page not found</h2>", status_code=404)
|
|
page = dict(row)
|
|
ws = page.get("workspace", "")
|
|
parts = ws.split("/") if "/" in ws else ["", ""]
|
|
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
|
|
sidebar = board_sidebar(request, owner, repo)
|
|
# Load sub-pages
|
|
with get_conn() as conn:
|
|
subs = conn.execute(
|
|
"SELECT id, title FROM pages WHERE parent_id=? ORDER BY updated_at DESC",
|
|
(page_id,),
|
|
).fetchall()
|
|
ctx = {**sidebar, "page": page, "sub_pages": [dict(s) for s in subs],
|
|
"page_favorited": False,
|
|
"page_share_mode": page.get("share_mode", "private"),
|
|
"page_published": bool(page.get("published", 0))}
|
|
template = env.get_template("page_editor.html")
|
|
response = template.render(**ctx)
|
|
return HTMLResponse(content=response, headers={"Cache-Control": "no-store, max-age=0"})
|
|
|
|
|
|
@router.get("/accounts", response_class=HTMLResponse)
|
|
async def accounts_page(request: Request):
|
|
"""Account management panel."""
|
|
from jinja2 import Environment, FileSystemLoader
|
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
|
sidebar = _sidebar_data(request, [])
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
with get_conn() as conn:
|
|
users = conn.execute("SELECT * FROM users ORDER BY created_at DESC").fetchall()
|
|
ctx = {**sidebar, "user": user, "users": [dict(u) for u in users]}
|
|
template = env.get_template("accounts.html")
|
|
return template.render(**ctx)
|
|
|
|
|
|
@router.get("/", response_class=HTMLResponse)
|
|
async def dashboard(
|
|
request: Request,
|
|
search: str = Query(default=""),
|
|
show_archived: bool = Query(default=False),
|
|
):
|
|
try:
|
|
repos = await gitea.get_user_repos(page=1, limit=50)
|
|
if search:
|
|
q = search.lower()
|
|
repos = [r for r in repos if q in r.get("full_name", "").lower()
|
|
or q in (r.get("description") or "").lower()]
|
|
if not show_archived:
|
|
repos = [r for r in repos if not r.get("archived", False)]
|
|
repos.sort(key=lambda r: r.get("updated_at", ""), reverse=True)
|
|
except Exception as e:
|
|
logger.error("Dashboard error: %s", e)
|
|
repos = []
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
|
sidebar = _sidebar_data(request, repos)
|
|
template = env.get_template("dashboard.html")
|
|
return template.render(request=request, repos=repos, search=search,
|
|
show_archived=show_archived, **sidebar)
|