Files
ss-tools/specs/034-task-status-center/data-model.md
busya 33ee976c48 feat(reports): Task Status Center — unified /reports dashboard
Страница /reports трансформирована в Центр статусов задач:

Backend:
- GET /api/reports/summary — агрегированные счётчики тип×статус (5 корзин)
- GET/PUT /api/settings/reports — глобальные настройки отчётов
- _filter_tasks_by_rbac() — row-level фильтрация по роли
- normalize_task_report: LLM-валидация с ошибками → FAILED/PARTIAL
- get_summary(): 5 корзин pending/running/awaiting_input/success/failed

Frontend:
- TaskCenterModel.svelte.ts (400 строк) — Screen Model
- SummaryPanel — сводная панель с цветовым кодированием и active filter
- ReportCard — humanized labels, duration, task_id, failed border
- FilterBar — search + sort + time range с label'ами
- Pagination — showingText, уникальные id для select
- Quick views: «Упавшие», «В работе», «Успешные»
- TaskDrawer: scroll-to-error, footer скрыт для terminal, «Н/Д» fix

Тесты: 48 backend + 38 frontend (2521 всего)
Build:   Console errors: 0
2026-07-02 18:53:58 +03:00

13 KiB

#region DataModel [C:4] [TYPE ADR] [SEMANTICS data-model, schema, pydantic, typescript, dto, task-status-center] @BRIEF Data model for the Task Status Center feature — backend Pydantic schemas (new + modified), frontend TypeScript DTOs, and the cross-stack contract bridge. @RATIONALE The data model defines WHAT flows between backend and frontend. Without explicit DTO contracts, type drift between Python and TypeScript is inevitable over long-horizon development. @REJECTED Auto-generating TypeScript from Pydantic was rejected (R7) — the schema set is small enough that manual sync is proportionate and avoids build-step complexity.

1. Entity Relationship Summary

TaskRecord (SQLAlchemy, persisted)
  └── Task (Pydantic, in-memory via TaskManager)
        └── TaskReport (Pydantic, normalized for UI)
              ├── ReportCollection (paginated list)
              ├── ReportDetailView (single task detail)
              └── TaskSummary (aggregated counts) ← NEW

TaskSummary
  └── TaskTypeSummary[]  (one per task_type)
        └── StatusCounts  (pending, running, awaiting_input, success, failed)

2. New Backend Schemas

2.1 TaskSummary (NEW)

# backend/src/models/report.py (add to existing module)

# #region TaskSummary [C:3] [TYPE Class] [SEMANTICS summary, aggregation, task, status, count]
# @ingroup ReportModels
# @BRIEF Aggregated task counts by type and status for the summary dashboard.
# @DATA_CONTRACT Output: TaskSummary → frontend interface TaskSummary
# @RELATION DEPENDS_ON -> [TaskReport]
# @TEST_CONTRACT TaskSummaryModel -> { invariants: ["all counts >= 0", "total == sum of all status counts across all types"] }
class StatusCounts(BaseModel):
    """Counts per status for a single task type."""
    pending: int = Field(default=0, ge=0)
    running: int = Field(default=0, ge=0)
    awaiting_input: int = Field(default=0, ge=0)
    success: int = Field(default=0, ge=0)
    failed: int = Field(default=0, ge=0)

class TaskTypeSummary(BaseModel):
    """Summary for one task type."""
    task_type: TaskType
    display_label: str
    icon_token: str
    counts: StatusCounts
    total: int = Field(default=0, ge=0)

class TaskSummary(BaseModel):
    """Full summary response for the summary dashboard."""
    by_type: list[TaskTypeSummary]
    total_tasks: int = Field(default=0, ge=0)
    active_tasks: int = Field(default=0, ge=0)  # pending + running + awaiting_input
# #endregion TaskSummary

2.2 Enhanced ReportQuery (MODIFIED)

The existing ReportQuery already supports task_types[], statuses[], search, time_from, time_to, sort_by, sort_order. No schema changes needed — the filtering logic in the service method will be enhanced to use these parameters more thoroughly.

Change: sort_by validator adds "created_at" to allowed values (currently only updated_at, status, task_type).

2.3 Enhanced TaskReport (MODIFIED)

No new fields needed. The existing TaskReport already carries all required fields for the spec:

  • report_id → task identifier in list
  • task_type → filter/summary grouping
  • status → filter/summary grouping + color coding
  • started_at → temporal display
  • updated_at → sort default
  • summary → display text
  • error_context → error display + next actions
  • source_ref → linked resource (dashboard, dataset, config)

2.4 No DB Schema Changes

No new database tables or columns. The feature works entirely with:

  • Existing task_records table (persisted task metadata)
  • Existing task_logs table (persisted logs for Task Drawer)
  • In-memory TaskManager.get_all_tasks() as the data source

3. New Frontend TypeScript DTOs

// frontend/src/types/reports.ts (NEW FILE)

// #region ReportsTypes [C:3] [TYPE Module] [SEMANTICS types, dto, reports, task-status-center]
// @BRIEF TypeScript DTOs matching backend Pydantic schemas for the Task Status Center.
// @DATA_CONTRACT Input: TypeScript interfaces → Output: typed API responses
// @RELATION DEPENDS_ON -> [ReportModels]  via @DATA_CONTRACT cross-stack
// @RATIONALE Typed API responses prevent field mismatch bugs and enable IDE autocomplete.

/** Canonical task types matching backend TaskType enum. */
export type TaskType =
  | 'llm_verification'
  | 'backup'
  | 'migration'
  | 'documentation'
  | 'clean_release'
  | 'unknown';

/** Normalized report status matching backend ReportStatus enum. */
export type ReportStatus =
  | 'success'
  | 'failed'
  | 'in_progress'
  | 'partial';

/** Error/recovery context for failed tasks. */
export interface ErrorContext {
  code: string | null;
  message: string;
  next_actions: string[];
}

/** Single task report — matches backend TaskReport. */
export interface TaskReport {
  report_id: string;
  task_id: string;
  task_type: TaskType;
  status: ReportStatus;
  started_at: string | null;
  updated_at: string;
  summary: string;
  details?: Record<string, unknown> | null;
  validation_record?: Record<string, unknown> | null;
  error_context: ErrorContext | null;
  source_ref?: Record<string, unknown> | null;
}

/** Query parameters for report list filtering — matches backend ReportQuery. */
export interface ReportQuery {
  page: number;
  page_size: number;
  task_types: TaskType[];
  statuses: ReportStatus[];
  time_from?: string | null;
  time_to?: string | null;
  search?: string | null;
  sort_by: 'updated_at' | 'created_at' | 'status' | 'task_type';
  sort_order: 'asc' | 'desc';
}

/** Paginated collection — matches backend ReportCollection. */
export interface ReportCollection {
  items: TaskReport[];
  total: number;
  page: number;
  page_size: number;
  has_next: boolean;
  applied_filters: ReportQuery;
}

/** Detailed view — matches backend ReportDetailView. */
export interface ReportDetailView {
  report: TaskReport;
  timeline: Record<string, unknown>[];
  diagnostics: Record<string, unknown> | null;
  next_actions: string[];
}

// --- NEW for Task Status Center ---

/** Status counts for one task type in the summary. */
export interface StatusCounts {
  pending: number;
  running: number;
  awaiting_input: number;
  success: number;
  failed: number;
}

/** Per-type summary entry. */
export interface TaskTypeSummary {
  task_type: TaskType;
  display_label: string;
  icon_token: string;
  counts: StatusCounts;
  total: number;
}

/** Full summary response — matches backend TaskSummary. */
export interface TaskSummary {
  by_type: TaskTypeSummary[];
  total_tasks: number;
  active_tasks: number;
}

/** WebSocket task status event from /ws/task-events. */
export interface TaskStatusEvent {
  type: 'task_status';
  task_id: string;
  task: {
    id: string;
    plugin_id: string;
    status: string;
    started_at: string | null;
    finished_at: string | null;
    user_id: string | null;
    params: Record<string, unknown> | null;
    result: Record<string, unknown> | null;
  };
}

/** Filter state for the Task Center UI (broader than ReportQuery). */
export interface TaskCenterFilter {
  task_types: TaskType[];
  statuses: ReportStatus[];
  search: string;
  time_range: '1h' | '24h' | '7d' | '30d' | 'all';
  sort_by: 'updated_at' | 'created_at' | 'status' | 'task_type';
  sort_order: 'asc' | 'desc';
}

/** UI screen states for the Task Center page. */
export type ScreenState =
  | 'loading'
  | 'ready'
  | 'empty'
  | 'error'
  | 'disconnected'
  | 'reconnecting';
// #endregion ReportsTypes

4. Cross-Stack Contract Bridge

Backend (Pydantic)                    Frontend (TypeScript)
─────────────────                     ─────────────────────
TaskType (str Enum)      ←──→        TaskType (union type)
ReportStatus (str Enum)  ←──→        ReportStatus (union type)
TaskReport (BaseModel)   ←──→        interface TaskReport
ReportQuery (BaseModel)  ←──→        interface ReportQuery
ReportCollection         ←──→        interface ReportCollection
ReportDetailView         ←──→        interface ReportDetailView
TaskSummary (NEW)        ←──→        interface TaskSummary
StatusCounts (NEW)       ←──→        interface StatusCounts
TaskTypeSummary (NEW)    ←──→        interface TaskTypeSummary

Each bridge pair carries @DATA_CONTRACT annotation referencing the counterpart:

  • Backend: @DATA_CONTRACT Output: TaskSummary → frontend interface TaskSummary
  • Frontend: @DATA_CONTRACT Input: TypeScript interface → Output: typed API response

5. Data Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│ BROWSER                                                         │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ TaskCenterModel (.svelte.ts)                              │  │
│  │                                                            │  │
│  │  loadInitialData()                                         │  │
│  │    ├─ GET /api/reports/summary ──────► TaskSummary         │  │
│  │    └─ GET /api/reports?page=1&... ──► ReportCollection     │  │
│  │                                                            │  │
│  │  connectWebSocket()                                        │  │
│  │    └─ ws://host/ws/task-events ────► TaskStatusEvent x N   │  │
│  │         │                                                  │  │
│  │         └─ 100ms debounce buffer                            │  │
│  │              └─ apply to tasks[] ──► $derived summary      │  │
│  │                                                            │  │
│  │  selectTask(taskId)                                        │  │
│  │    └─ openDrawerForTask(taskId) ──► TaskDrawer + logs WS  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ +page.svelte (thin render layer ~60 lines)                │  │
│  │   ├─ SummaryPanel   ← m.summary                           │  │
│  │   ├─ FilterBar      ← m.filters, m.applyFilter()          │  │
│  │   └─ TaskList       ← m.filteredTasks, m.selectTask()     │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ BACKEND (FastAPI)                                                │
│                                                                 │
│  GET /api/reports/summary  ───► ReportsService.get_summary()    │
│    └─ task_manager.get_all_tasks()                               │
│       └─ filter by RBAC (role-based)                             │
│          └─ normalize → aggregate → TaskSummary                  │
│                                                                 │
│  GET /api/reports?...      ───► ReportsService.list_reports()    │
│    └─ same flow → filter, sort, paginate → ReportCollection     │
│                                                                 │
│  ws://host/ws/task-events  ───► EventBus.broadcast_status()     │
│    └─ already broadcasts on every task status transition        │
│       └─ NO CODE CHANGES NEEDED                                  │
└─────────────────────────────────────────────────────────────────┘

#endregion DataModel