- 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]>
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""FlowDeck — v5.2.0 Forge abstraction (Gitea / GitHub).
|
|
|
|
``ForgeAdapter`` defines the minimal contract a forge client must expose for the
|
|
``projects`` table sync and the issue/board integration. ``GiteaAdapter`` wraps
|
|
the existing ``GiteaClient``; ``GitHubAdapter`` (in ``github_adapter.py``) is the
|
|
GitHub implementation.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
|
class ForgeAdapter(ABC):
|
|
"""Common forge API surface used by FlowDeck (v5.2.0)."""
|
|
|
|
kind = "base"
|
|
|
|
@abstractmethod
|
|
async def validate_token(self) -> bool:
|
|
"""True when the stored credentials still work."""
|
|
|
|
@abstractmethod
|
|
async def list_repos(self, page: int = 1) -> list[dict]:
|
|
"""List repositories for the authenticated user."""
|
|
|
|
@abstractmethod
|
|
async def get_repo_info(self, owner: str, repo: str) -> dict:
|
|
"""Repository metadata (default_branch, clone_url, language, …)."""
|
|
|
|
|
|
def normalize_repo(repo: dict, proj_type: str) -> dict:
|
|
"""Project a forge repo dict onto the ``projects`` table columns."""
|
|
full_name = repo.get("full_name", "") or repo.get("fullName", "")
|
|
owner, _, name = full_name.partition("/")
|
|
return {
|
|
"name": name or repo.get("name", ""),
|
|
"owner": owner or repo.get("owner", {}).get("login", "") if isinstance(repo.get("owner"), dict) else (owner or ""),
|
|
"proj_type": proj_type,
|
|
"forge_id": str(repo.get("id", "") or ""),
|
|
"clone_url": repo.get("clone_url", "") or repo.get("ssh_url", ""),
|
|
"default_branch": repo.get("default_branch", ""),
|
|
"language": repo.get("language", ""),
|
|
"description": (repo.get("description") or "") or "",
|
|
}
|
|
|
|
|
|
class GiteaAdapter(ForgeAdapter):
|
|
"""Adapt the existing GiteaClient to the ForgeAdapter contract."""
|
|
|
|
kind = "gitea"
|
|
|
|
def __init__(self, client) -> None: # client = GiteaClient instance
|
|
self._client = client
|
|
|
|
async def validate_token(self) -> bool:
|
|
try:
|
|
await self._client.get_user_repos(page=1, limit=1)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
async def list_repos(self, page: int = 1) -> list[dict]:
|
|
return await self._client.get_user_repos(page=page, limit=30)
|
|
|
|
async def get_repo_info(self, owner: str, repo: str) -> dict:
|
|
async with __import__("httpx").AsyncClient(timeout=15) as client:
|
|
resp = await client.get(
|
|
f"{self._client._base}/repos/{owner}/{repo}",
|
|
headers=self._client._headers,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|