From b10118eddc015497cc329e2bf2fc5e4858241b1f Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 8 Jul 2026 15:02:15 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20sidebar=20Kanban=20board=20+=20pages=20?= =?UTF-8?q?Markdown=20+=20boutons=20+=20et=20=F0=9F=93=9A=20fonctionnels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar: 'Kanban board' affichΓ© avant les 4 sous-vues - DB: nouvelle table 'pages' (workspace, title, content, parent_section) - API: POST /board/api/pages, GET/PUT /board/api/pages/{id} - UI: page_editor.html β€” Γ©diteur Markdown avec preview live - UI: boutons + crΓ©ent une page Markdown, bouton πŸ“š ouvre la page - Dashboard: sidebar_data utilise BDD pages + workspace - board.py: _sidebar_data enrichi (Kanban board + pages DB) --- app/db.py | 10 +++ app/routers/board.py | 89 ++++++++++++++++--- app/routers/dashboard.py | 50 +++++------ app/templates/base.html | 42 ++++++--- app/templates/page_editor.html | 152 +++++++++++++++++++++++++++++++++ 5 files changed, 295 insertions(+), 48 deletions(-) create mode 100644 app/templates/page_editor.html diff --git a/app/db.py b/app/db.py index 4aec95a..484350d 100644 --- a/app/db.py +++ b/app/db.py @@ -130,6 +130,16 @@ def init_db(): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_owner, project_name, keyword) ); + + CREATE TABLE IF NOT EXISTS pages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + workspace TEXT NOT NULL, + title TEXT NOT NULL DEFAULT 'New page', + content TEXT NOT NULL DEFAULT '', + parent_section TEXT DEFAULT 'Private', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); """) conn.commit() diff --git a/app/routers/board.py b/app/routers/board.py index 3240363..5b6d66b 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -115,18 +115,34 @@ def _map_issue_to_card(issue: dict, owner: str = "", repo: str = "") -> dict: } -def _sidebar_data(request: Request, owner: str, repo: str) -> dict: +def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict: user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) ws = user.get("login", "Bruno") if user else "Bruno" - recent = [{"id": f"{owner}/{repo}", "name": repo, "icon": "πŸ“", - "url": f"/board/{owner}/{repo}", "active": True, "indent": 0}] - for sv in ["Detailed board", "Table view", "Status overview", "Team Load"]: - recent.append({"id": f"{owner}/{repo}/{sv.lower().replace(' ', '-')}", - "name": sv, "icon": "Β·", "url": f"/board/{owner}/{repo}", - "active": False, "indent": 1}) + recent = [] + if owner and repo: + recent = [ + {"id": f"{owner}/{repo}", "name": "Kanban board", "icon": "πŸ“", + "url": f"/board/{owner}/{repo}", "active": True, "indent": 0}, + ] + for sv in ["Detailed board", "Table view", "Status overview", "Team Load"]: + recent.append({"id": f"{owner}/{repo}/{sv.lower().replace(' ', '-')}", + "name": sv, "icon": "Β·", "url": f"/board/{owner}/{repo}", + "active": False, "indent": 1}) + # Load user-created pages from DB + with get_conn() as conn: + pages = conn.execute( + "SELECT id, title, updated_at FROM pages WHERE workspace=? ORDER BY updated_at DESC LIMIT 10", + (ws,), + ).fetchall() + for p in pages: + recent.append({ + "id": f"page/{p['id']}", "name": p["title"], "icon": "πŸ“„", + "url": f"/pages/{p['id']}", "active": False, "indent": 0, + }) return {"workspace_name": ws, "workspace_initial": ws[0].upper() if ws else "B", - "current_page": repo, "last_edited": "2m ago", - "recent_pages": recent, "private_pages": recent[:1], "user": user} + "current_page": repo or "Dashboard", "last_edited": "now", + "recent_pages": recent, "private_pages": [r for r in recent if r.get("active")], + "user": user} def _extract_ai_keywords(owner: str, repo: str, labels: list[dict], body: str = ""): @@ -391,7 +407,60 @@ async def extract_ai_keywords(owner: str, repo: str): return {"status": "ok", "issues_scanned": len(issues)} -# ═══════════ v0.9.0: Sync endpoint ═══════════ +# ═══════════ Pages Markdown ═══════════ + +@router.post("/api/pages") +async def create_page(request: Request, title: str = Query(default="New page"), + section: str = Query(default="Private")): + """Create a new Markdown page.""" + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + ws = user.get("login", "Bruno") if user else "Bruno" + with get_conn() as conn: + cur = conn.execute( + "INSERT INTO pages (workspace, title, parent_section) VALUES (?,?,?)", + (ws, title, section), + ) + conn.commit() + page_id = cur.lastrowid + return {"status": "ok", "id": page_id, "title": title} + + +@router.get("/api/pages/{page_id}") +async def get_page(page_id: int): + """Get a Markdown page.""" + with get_conn() as conn: + row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone() + if not row: + raise HTTPException(404, "Page not found") + return dict(row) + + +@router.put("/api/pages/{page_id}") +async def update_page(page_id: int, title: str = Query(default=""), + content: str = Query(default="")): + """Update a page's title and/or content.""" + with get_conn() as conn: + if title: + conn.execute("UPDATE pages SET title=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (title, page_id)) + if content: + conn.execute("UPDATE pages SET content=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (content, page_id)) + conn.commit() + return {"status": "ok"} + + +@router.get("/pages/{page_id}", response_class=HTMLResponse) +async def view_page(request: Request, page_id: int): + """Render a page as HTML.""" + from jinja2 import Environment, FileSystemLoader + 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("

Page not found

", status_code=404) + sidebar = _sidebar_data(request) + ctx = {**sidebar, "page": dict(row)} + template = env.get_template("page_editor.html") + return template.render(**ctx) @router.post("/api/sync/{owner}/{repo}") async def sync_project(owner: str, repo: str): diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 34e137f..9077419 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -8,41 +8,46 @@ 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: - """Build sidebar context from Gitea repos.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) - workspace_name = user.get("login", "Bruno") if user else "Bruno" - workspace_initial = workspace_name[0].upper() if workspace_name else "B" + ws = user.get("login", "Bruno") if user else "Bruno" + initial = ws[0].upper() if ws else "B" - # Build recent pages from repos recent_pages = [] - private_pages = [] for repo in repos[:10]: full_name = repo.get("full_name", "") - parts = full_name.split("/") if "/" in full_name else [workspace_name, full_name] - page = { + recent_pages.append({ "id": full_name, "name": repo.get("name", full_name), "icon": "πŸ“", "url": f"/board/{full_name}", "active": False, "indent": 0, - } - recent_pages.append(page) - private_pages.append({**page}) + }) + + # Load user-created pages + with get_conn() as conn: + pages = conn.execute( + "SELECT id, title FROM pages WHERE workspace=? ORDER BY updated_at DESC LIMIT 10", + (ws,), + ).fetchall() + for p in pages: + recent_pages.append({ + "id": f"page/{p['id']}", "name": p["title"], "icon": "πŸ“„", + "url": f"/pages/{p['id']}", "active": False, "indent": 0, + }) return { - "workspace_name": workspace_name, - "workspace_initial": workspace_initial, - "current_page": "Dashboard", - "last_edited": "now", + "workspace_name": ws, "workspace_initial": initial, + "current_page": "Dashboard", "last_edited": "now", "recent_pages": recent_pages, - "private_pages": private_pages, + "private_pages": [p for p in recent_pages if p["id"].startswith("page/")], "user": user, } @@ -53,33 +58,22 @@ async def dashboard( search: str = Query(default=""), show_archived: bool = Query(default=False), ): - """Dashboard: liste tous les projets Gitea (user + orgs).""" 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, - ) + return template.render(request=request, repos=repos, search=search, + show_archived=show_archived, **sidebar) diff --git a/app/templates/base.html b/app/templates/base.html index 28dc812..1250801 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -48,8 +48,8 @@ β–Ά Recents