v5.2.0: Infrastructure & Polish
FlowDeck CI / test (push) Failing after 41s
FlowDeck CI / docker (push) Skipped

- 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:
2026-09-10 23:47:35 -04:00
co-authored by Bruno
parent e9b6244ef0
commit 5c350ff8f6
77 changed files with 2447 additions and 293 deletions
+6 -7
View File
@@ -8,7 +8,6 @@ from __future__ import annotations
import logging
import re
from typing import Optional
from app.db import get_conn
from app.migrations import fts5_available
@@ -24,7 +23,7 @@ def _fts_terms(query: str) -> list[str]:
return [t.replace('"', '""') for t in tokens if t]
def _fts_match(query: str) -> Optional[str]:
def _fts_match(query: str) -> str | None:
"""Build a MATCH expression, or None when the query is not FTS-safe."""
terms = _fts_terms(query)
if not terms:
@@ -78,7 +77,7 @@ def _workspace_name(conn, workspace_id) -> str:
return ""
def _scope_where(user_id: Optional[int]) -> tuple[str, list]:
def _scope_where(user_id: int | None) -> tuple[str, list]:
"""SQL filter restricting results to the user's accessible workspaces."""
if user_id is None:
return "1=1", []
@@ -92,7 +91,7 @@ def _scope_where(user_id: Optional[int]) -> tuple[str, list]:
# ── Search entry point ──────────────────────────────────────────────────────
def search(query: str, user_id: Optional[int] = None, limit: int = 20) -> dict:
def search(query: str, user_id: int | None = None, limit: int = 20) -> dict:
"""Return unified search results: ``{pages: [...], collections: [...]}``."""
q = (query or "").strip()
if not q:
@@ -104,7 +103,7 @@ def search(query: str, user_id: Optional[int] = None, limit: int = 20) -> dict:
return {"pages": pages, "collections": collections}
def _search_pages(conn, query: str, user_id: Optional[int], limit: int) -> list:
def _search_pages(conn, query: str, user_id: int | None, limit: int) -> list:
like = f"%{query}%"
scope, params = _scope_where(user_id)
@@ -142,7 +141,7 @@ def _search_pages(conn, query: str, user_id: Optional[int], limit: int) -> list:
return _page_rows_to_results(conn, rows)
def _search_collections(conn, query: str, user_id: Optional[int], limit: int) -> list:
def _search_collections(conn, query: str, user_id: int | None, limit: int) -> list:
like = f"%{query}%"
scope, params = _scope_where(user_id)
rows = conn.execute(
@@ -184,4 +183,4 @@ def _page_rows_to_results(conn, rows) -> list:
"excerpt": excerpt[:160],
"url": f"/pages/{r['id']}",
})
return results
return results