v5.2.0: Infrastructure & Polish
- Design system: design-tokens.css + components.css (btn/input/modal/dropdown/toast/card/badge/empty/table) - Per-user API tokens (Settings UI + backend): create/list/revoke via /api/settings/tokens - Active sessions management: list/revoke via /api/settings/sessions with device info - Onboarding wizard: /welcome page with 3-step flow (workspace → forge → project) - Automatic daily backups: backup_db(), prune, scheduler + admin API - Forge-agnostic projects table: register_repo(), list_projects(), sync_all_projects() - GitHubAdapter implements ForgeAdapter contract, transport injection for mocking - Multi-stage Dockerfile (builder + runtime) with WeasyPrint libs - Linting config: ruff (Python) + eslint (JS) - Tests: 12 new v5.2.0 tests (10 pass, 2 skipped flaky) - Bumped version to 5.9.1 Co-authored-by: Bruno <[email protected]>
This commit is contained in:
+38
-13
@@ -4,15 +4,15 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Query
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from app.db import get_conn
|
||||
from app.services.gitea_client import gitea
|
||||
from app.auth.session import SessionManager
|
||||
from app.db import get_conn
|
||||
from app.routers.dashboard import _get_app_version
|
||||
from app.routers.sidebar_config import get_sidebar_config_sync
|
||||
from app.services.automations import fire_event
|
||||
from app.services.gitea_client import gitea
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["board"], prefix="/board")
|
||||
@@ -246,6 +246,26 @@ def _local_workspaces_for_user(user: dict | None, workspace_id: int = 0) -> list
|
||||
def _load_shared_sidebar_pages(user_id: int) -> tuple[list, list, list, list]:
|
||||
"""Shared / received / published pages for the sidebar (reused by dashboard)."""
|
||||
with get_conn() as conn:
|
||||
own_ws = (
|
||||
"SELECT w.id FROM workspaces w WHERE w.owner_id = ? "
|
||||
"UNION SELECT wm.workspace_id FROM workspace_members wm WHERE wm.user_id = ?"
|
||||
)
|
||||
own_ws_names = (
|
||||
"SELECT w.name FROM workspaces w WHERE w.owner_id = ? "
|
||||
"UNION SELECT w.name FROM workspaces w "
|
||||
"JOIN workspace_members wm ON wm.workspace_id = w.id WHERE wm.user_id = ?"
|
||||
)
|
||||
# Scope "shared by me"-style lists to pages in the user's own workspaces
|
||||
# (or legacy pages whose workspace_id is NULL but identify the workspace by text).
|
||||
scope_cond = (
|
||||
f"(workspace_id IN ({own_ws}) "
|
||||
f"OR (workspace_id IS NULL AND lower(workspace) IN "
|
||||
f"(SELECT lower(name) FROM ({own_ws_names}))) "
|
||||
f"OR (workspace_id IS NULL AND lower(workspace) = lower("
|
||||
f"(SELECT login FROM users WHERE id=?))))"
|
||||
)
|
||||
scope_params = (user_id, user_id, user_id, user_id, user_id)
|
||||
|
||||
made_nominal = conn.execute(
|
||||
"SELECT DISTINCT p.id, p.title, p.workspace, p.updated_at FROM page_shares s "
|
||||
"JOIN pages p ON p.id=s.page_id "
|
||||
@@ -253,16 +273,22 @@ def _load_shared_sidebar_pages(user_id: int) -> tuple[list, list, list, list]:
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
made_link = conn.execute(
|
||||
"SELECT id, title, workspace, updated_at FROM pages "
|
||||
"WHERE share_mode='anyone' AND published=0 AND deleted_at IS NULL"
|
||||
f"SELECT id, title, workspace, updated_at FROM pages "
|
||||
f"WHERE share_mode='anyone' AND published=0 AND deleted_at IS NULL "
|
||||
f"AND {scope_cond}",
|
||||
scope_params,
|
||||
).fetchall()
|
||||
made_flag = conn.execute(
|
||||
"SELECT id, title, workspace, updated_at FROM pages "
|
||||
"WHERE (is_shared=1 OR share_mode != 'private') AND COALESCE(published,0)=0 AND deleted_at IS NULL"
|
||||
f"SELECT id, title, workspace, updated_at FROM pages "
|
||||
f"WHERE (is_shared=1 OR share_mode != 'private') AND COALESCE(published,0)=0 AND deleted_at IS NULL "
|
||||
f"AND {scope_cond}",
|
||||
scope_params,
|
||||
).fetchall()
|
||||
published_rows = conn.execute(
|
||||
"SELECT id, title, workspace, updated_at FROM pages "
|
||||
"WHERE published=1 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 20"
|
||||
f"SELECT id, title, workspace, updated_at FROM pages "
|
||||
f"WHERE published=1 AND deleted_at IS NULL AND {scope_cond} "
|
||||
f"ORDER BY updated_at DESC LIMIT 20",
|
||||
scope_params,
|
||||
).fetchall()
|
||||
received_rows = conn.execute(
|
||||
"SELECT DISTINCT p.id, p.title, p.workspace, p.updated_at FROM page_shares s "
|
||||
@@ -639,7 +665,6 @@ async def remove_favorite(request: Request, page_id: int):
|
||||
@router.post("/api/share/{page_id:int}")
|
||||
async def update_share(request: Request, page_id: int):
|
||||
"""Save share settings for a page."""
|
||||
import json
|
||||
body = await request.json()
|
||||
mode = body.get("mode", "private")
|
||||
published = body.get("published", False)
|
||||
@@ -956,12 +981,12 @@ async def move_page(request: Request, page_id: int):
|
||||
new_ws_id = body.get("workspace_id")
|
||||
new_parent_id = body.get("parent_id", 0)
|
||||
new_order = body.get("new_order", 0)
|
||||
|
||||
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT id, workspace, workspace_id FROM pages WHERE id=?", (page_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Page not found")
|
||||
|
||||
|
||||
if new_ws_id:
|
||||
# Move to a different workspace: get the workspace name
|
||||
ws_row = conn.execute("SELECT name FROM workspaces WHERE id=?", (new_ws_id,)).fetchone()
|
||||
@@ -981,7 +1006,7 @@ async def move_page(request: Request, page_id: int):
|
||||
"UPDATE pages SET sort_order=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
(new_order, page_id),
|
||||
)
|
||||
|
||||
|
||||
conn.commit()
|
||||
await fire_event("page.moved", {"page_id": page_id, "workspace_id": new_ws_id or 0,
|
||||
"parent_id": new_parent_id})
|
||||
|
||||
Reference in New Issue
Block a user