feat: sidebar Kanban board + pages Markdown + boutons + et 📚 fonctionnels
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- 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)
This commit is contained in:
2026-07-08 15:02:15 -04:00
parent a5e91cf8c7
commit b10118eddc
5 changed files with 295 additions and 48 deletions
+79 -10
View File
@@ -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("<h2>Page not found</h2>", 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):
+22 -28
View File
@@ -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)