diff --git a/app/db.py b/app/db.py index 945e16f..0b33be6 100644 --- a/app/db.py +++ b/app/db.py @@ -244,14 +244,16 @@ def init_db(): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); + -- v2.2.0: Migrate favorites β drop old schema referencing collection_pages + DROP TABLE IF EXISTS favorites; + CREATE TABLE IF NOT EXISTS favorites ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), - page_id INTEGER REFERENCES collection_pages(id), - collection_id INTEGER REFERENCES collections(id), + page_id INTEGER NOT NULL REFERENCES pages(id), position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, page_id, collection_id) + UNIQUE(user_id, page_id) ); CREATE TABLE IF NOT EXISTS database_templates ( diff --git a/app/middleware/csrf.py b/app/middleware/csrf.py index 29f94a9..b1af78a 100644 --- a/app/middleware/csrf.py +++ b/app/middleware/csrf.py @@ -16,7 +16,7 @@ class CSRFMiddleware(BaseHTTPMiddleware): """ SAFE_METHODS = {"GET", "HEAD", "OPTIONS"} - EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/board/api/pages", "/db/", "/workspace"} + EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/board/api/pages", "/board/api/favorites", "/db/", "/workspace"} async def dispatch(self, request: Request, call_next): # Webhook receiver, OAuth callback, and internal API are exempt diff --git a/app/routers/board.py b/app/routers/board.py index 8615e4a..15bfc4b 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -169,10 +169,34 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict: recent.append(p) # Private pages: same as recent but filtered for page/ items (non-board views) private_items = [r for r in recent if r.get("active") or r["id"].startswith("page/")] + + # Load favorite pages from DB + 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() + favorites = [] + for r in fav_rows: + favorites.append({ + "id": f"page/{r['id']}", + "db_id": r["id"], + "name": r["title"] or "New page", + "icon": "π", + "url": f"/pages/{r['id']}", + "active": False, + "indent": 0, + "depth": 0, + "has_children": False, + "children": [], + }) + return {"workspace_name": ws_name, "workspace_initial": ws_name[0].upper() if ws_name else "B", "current_page": repo or "Dashboard", "last_edited": "now", "recent_pages": recent, "private_pages": private_items, - "favorite_pages": [], "user": user, + "favorite_pages": favorites, "user": user, "workspace_key": ws_key} @@ -314,6 +338,51 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s template = env.get_template("library.html") return template.render(**sidebar) +# βββββββββββ Favorites API βββββββββββ + +@router.get("/api/favorites") +async def list_favorites(request: Request): + """List favorited page IDs for the current user.""" + 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: + rows = conn.execute( + "SELECT page_id FROM favorites WHERE user_id=? ORDER BY position", (uid,) + ).fetchall() + return {"favorites": [r["page_id"] for r in rows]} + + +@router.post("/api/favorites/{page_id:int}") +async def add_favorite(request: Request, page_id: int): + """Add a page to favorites.""" + 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: + existing = conn.execute( + "SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id) + ).fetchone() + if not existing: + pos = conn.execute( + "SELECT COALESCE(MAX(position), -1) + 1 FROM favorites WHERE user_id=?", (uid,) + ).fetchone()[0] + conn.execute( + "INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,?)", + (uid, page_id, pos), + ) + conn.commit() + return {"status": "added", "page_id": page_id} + + +@router.delete("/api/favorites/{page_id:int}") +async def remove_favorite(request: Request, page_id: int): + """Remove a page from favorites.""" + 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: + conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id)) + conn.commit() + return {"status": "removed", "page_id": page_id} + # βββββββββββ Trash (must be before /{owner}/{repo} catch-all) βββββββββββ @router.get("/api/trash") diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index d994c4a..35c2c0c 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -109,13 +109,36 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s 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"] = [p for p, r in zip(all_pages, rows) if (r["parent_section"] or "") == "Favorites"] - sidebar["lib_shared"] = [p for p, r in zip(all_pages, rows) if (r["parent_section"] or "") == "Shared"] - sidebar["lib_private"] = [p for p, r in zip(all_pages, rows) if (r["parent_section"] or "Private") == "Private"] - sidebar["lib_has_favorites"] = len(sidebar["lib_favorites"]) > 0 - sidebar["lib_has_shared"] = len(sidebar["lib_shared"]) > 0 + sidebar["lib_favorites"] = lib_favs + sidebar["lib_shared"] = [] + sidebar["lib_private"] = [p for p in all_pages if p.get("source") == "π Private"] + sidebar["lib_has_favorites"] = len(lib_favs) > 0 + sidebar["lib_has_shared"] = False template = env.get_template("library.html") return template.render(**sidebar) diff --git a/app/templates/base.html b/app/templates/base.html index 95023d9..1b0aaab 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -289,7 +289,8 @@