Source code for corvix.web.schemas

"""Typed response schemas for the Corvix web API.

These dataclasses are the single source of truth for the JSON shapes returned
by the ``/api/v1`` route handlers. Litestar serializes them directly and
auto-generates an OpenAPI document from their annotations; the frontend's
TypeScript types are code-generated from that document (see
``scripts/export_openapi.py`` and ``frontend/src/api-types.gen.ts``).

Every field is required (no defaults) so the generated OpenAPI schema marks
each property as required and the TypeScript types never become accidentally
optional. ``str | None`` fields map to a nullable-but-present property, matching
how the handlers always emit the key.
"""

from __future__ import annotations

from dataclasses import dataclass

from corvix.config.notifications import NotificationsConfig
from corvix.dashboarding import DashboardData, DashboardItem


@dataclass(slots=True)
[docs] class DashboardItemResponse: """A single notification row rendered by the dashboard table."""
[docs] account_id: str
[docs] account_label: str
[docs] thread_id: str
[docs] repository: str
[docs] reason: str
[docs] subject_type: str
[docs] subject_title: str
[docs] unread: bool
[docs] updated_at: str
[docs] score: float
[docs] web_url: str | None
[docs] matched_rules: list[str]
[docs] actions_taken: list[str]
@dataclass(slots=True)
[docs] class DashboardGroupResponse: """A group of dashboard items (e.g. grouped by repository or reason)."""
[docs] name: str
[docs] items: list[DashboardItemResponse]
@dataclass(slots=True)
[docs] class DashboardSummaryResponse: """Aggregate counts shown in the dashboard shell."""
[docs] unread_items: int
[docs] read_items: int
[docs] group_count: int
[docs] repository_count: int
[docs] reason_count: int
@dataclass(slots=True)
[docs] class AccountErrorResponse: """A per-account fetch failure recorded during a poll cycle."""
[docs] account_id: str
[docs] account_label: str
[docs] error: str
@dataclass(slots=True)
[docs] class PollerStatusResponse: """Poller health surfaced to the UI for the staleness warning banner."""
[docs] status: str
[docs] last_poll_time: str | None
[docs] last_error: str | None
[docs] last_error_time: str | None
[docs] stale: bool
[docs] account_errors: list[AccountErrorResponse]
@dataclass(slots=True)
[docs] class BrowserTabNotificationsConfigResponse: """In-tab browser notification settings echoed to the frontend."""
[docs] enabled: bool
[docs] max_per_cycle: int
[docs] cooldown_seconds: int
@dataclass(slots=True)
[docs] class NotificationsConfigResponse: """Notification configuration relevant to the browser client."""
[docs] enabled: bool
[docs] browser_tab: BrowserTabNotificationsConfigResponse
@dataclass(slots=True)
[docs] class SnapshotResponse: """Full dashboard snapshot returned by ``GET /api/v1/snapshot``."""
[docs] name: str
[docs] include_read: bool
[docs] sort_by: str
[docs] descending: bool
[docs] generated_at: str | None
[docs] groups: list[DashboardGroupResponse]
[docs] total_items: int
[docs] summary: DashboardSummaryResponse
[docs] dashboard_names: list[str]
[docs] poller: PollerStatusResponse
[docs] notifications_config: NotificationsConfigResponse | None
@dataclass(slots=True)
[docs] class RuleSnippetsResponse: """Prefilled ignore-rule snippets for a single notification."""
[docs] dashboard_name: str
[docs] dashboard_ignore_rule_snippet: str
[docs] global_exclude_rule_snippet: str
[docs] dashboard_ignore_rule_with_context_snippet: str | None
[docs] global_exclude_rule_with_context_snippet: str | None
[docs] has_context: bool
[docs] def _dashboard_item_response(item: DashboardItem) -> DashboardItemResponse: """Convert a ``dashboarding.DashboardItem`` into its response schema.""" return DashboardItemResponse( account_id=item.account_id, account_label=item.account_label, thread_id=item.thread_id, repository=item.repository, reason=item.reason, subject_type=item.subject_type, subject_title=item.subject_title, unread=item.unread, updated_at=item.updated_at, score=item.score, web_url=item.web_url, matched_rules=list(item.matched_rules), actions_taken=list(item.actions_taken), )
[docs] def build_snapshot_response( *, data: DashboardData, dashboard_names: list[str], poller: PollerStatusResponse, notifications_config: NotificationsConfig | None, ) -> SnapshotResponse: """Assemble a typed :class:`SnapshotResponse` from dashboard data. Centralizes the mapping from the internal ``DashboardData`` dataclass (plus the already-resolved poller status and config state) to the wire schema so the route handler stays thin and the contract lives in one place. """ notif: NotificationsConfigResponse | None = None if notifications_config is not None: browser = notifications_config.browser_tab notif = NotificationsConfigResponse( enabled=notifications_config.enabled, browser_tab=BrowserTabNotificationsConfigResponse( enabled=browser.enabled, max_per_cycle=browser.max_per_cycle, cooldown_seconds=browser.cooldown_seconds, ), ) return SnapshotResponse( name=data.name, include_read=data.include_read, sort_by=data.sort_by, descending=data.descending, generated_at=data.generated_at, groups=[ DashboardGroupResponse( name=group.name, items=[_dashboard_item_response(item) for item in group.items], ) for group in data.groups ], total_items=data.total_items, summary=DashboardSummaryResponse( unread_items=data.summary.unread_items, read_items=data.summary.read_items, group_count=data.summary.group_count, repository_count=data.summary.repository_count, reason_count=data.summary.reason_count, ), dashboard_names=dashboard_names, poller=poller, notifications_config=notif, )