From d9669698b88d419c87625e8b81f9e04735973b5f Mon Sep 17 00:00:00 2001 From: busya Date: Fri, 22 May 2026 16:20:39 +0300 Subject: [PATCH] =?UTF-8?q?feat(031):=20Maintenance=20Banner=20for=20Dashb?= =?UTF-8?q?oards=20=E2=80=94=20full=20spec=E2=86=92plan=E2=86=92tasks=20pa?= =?UTF-8?q?ckage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec: 5 user stories (P1-P3), 17 FRs, 7 SCs, 12 edge cases, RBAC matrix Plan: 7 research decisions, 15 semantic contracts (C1-C4) Data: 4 SQLAlchemy models (Banner entity enforces one-chart-per-dashboard) UX: async-only API, banner template with optional variables, admin settings UI Tasks: 64 tasks across 8 phases, 12 reused frontend components Workflow: component reuse scan mandated in speckit.plan/tasks + semantics-svelte Constitution: filled with 7 architecture principles from ADRs --- .kilocode/rules/specify-rules.md | 3 + .opencode/command/speckit.plan.md | 29 +++ .opencode/command/speckit.tasks.md | 12 ++ .opencode/skills/semantics-svelte/SKILL.md | 6 + .specify/memory/constitution.md | 99 ++++++---- .../contracts/modules.md | 171 +++++++----------- specs/031-maintenance-banner/data-model.md | 160 ++++++++++++---- specs/031-maintenance-banner/plan.md | 4 +- specs/031-maintenance-banner/quickstart.md | 2 +- specs/031-maintenance-banner/research.md | 16 +- specs/031-maintenance-banner/spec.md | 45 ++++- specs/031-maintenance-banner/tasks.md | 51 +++--- specs/031-maintenance-banner/ux_reference.md | 45 ++++- 13 files changed, 414 insertions(+), 229 deletions(-) diff --git a/.kilocode/rules/specify-rules.md b/.kilocode/rules/specify-rules.md index 778f78bf..2375a331 100644 --- a/.kilocode/rules/specify-rules.md +++ b/.kilocode/rules/specify-rules.md @@ -5,6 +5,8 @@ Auto-generated from all feature plans. Last updated: 2026-05-08 ## Active Technologies - Python 3.9+ (backend), JavaScript/TypeScript — Svelte 5 runes (frontend) + FastAPI 0.104+, Pydantic v2, SQLAlchemy (backend); SvelteKit 2.x, Svelte 5.x, Vite 7.x, Tailwind CSS 3.x (frontend) (030-dataset-lifecycle-workspace) - PostgreSQL 16 (ss-tools own DB); no schema changes in this feature (030-dataset-lifecycle-workspace) +- Python 3.9+ (backend), JavaScript/TypeScript (frontend Svelte 5 runes) + FastAPI 0.126, SQLAlchemy 2.0, APScheduler 3.11 (backend); SvelteKit 2.x, Svelte 5.43, Vite 7.x, Tailwind CSS 3.x (frontend) (031-maintenance-banner) +- PostgreSQL 16 (dedicated ss-tools DB — not Superset metadata DB per ADR-0003) (031-maintenance-banner) - Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5) + FastAPI 0.115+, SQLAlchemy 2.0+, APScheduler 3.x, Pydantic v2 (backend); SvelteKit 2.x, Svelte 5.43+, Vite 7.x, Tailwind CSS 3.x (frontend) (028-llm-datasource-supeset) @@ -25,6 +27,7 @@ cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLO Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5): Follow standard conventions ## Recent Changes +- 031-maintenance-banner: Added Python 3.9+ (backend), JavaScript/TypeScript (frontend Svelte 5 runes) + FastAPI 0.126, SQLAlchemy 2.0, APScheduler 3.11 (backend); SvelteKit 2.x, Svelte 5.43, Vite 7.x, Tailwind CSS 3.x (frontend) - 030-dataset-lifecycle-workspace: Added Python 3.9+ (backend), JavaScript/TypeScript — Svelte 5 runes (frontend) + FastAPI 0.104+, Pydantic v2, SQLAlchemy (backend); SvelteKit 2.x, Svelte 5.x, Vite 7.x, Tailwind CSS 3.x (frontend) - 028-llm-datasource-supeset: Added Python 3.13+ (backend), JavaScript/TypeScript (frontend Svelte 5) + FastAPI 0.115+, SQLAlchemy 2.0+, APScheduler 3.x, Pydantic v2 (backend); SvelteKit 2.x, Svelte 5.43+, Vite 7.x, Tailwind CSS 3.x (frontend) diff --git a/.opencode/command/speckit.plan.md b/.opencode/command/speckit.plan.md index 213272c2..53aeae01 100644 --- a/.opencode/command/speckit.plan.md +++ b/.opencode/command/speckit.plan.md @@ -71,6 +71,35 @@ Use `[NEED_CONTEXT: target]` instead of inventing relation targets, DTO names, o ## Phase 1: Design, ADR Continuity, and Contracts +### Frontend Component Reuse Scan (MANDATORY — before contract generation) + +Before designing any new Svelte component, execute a **component inventory scan** of the existing codebase to maximise reuse and prevent duplicate primitives. Use a subagent with `subagent_type: "explore"` to scan: + +**Scan targets** (priority order): +1. `frontend/src/lib/ui/` — design-system atoms: `Button.svelte`, `Select.svelte`, `Input.svelte`, `Card.svelte` +2. `frontend/src/lib/components/ui/` — composite UI widgets: `SearchableMultiSelect.svelte`, `MultiSelect.svelte` +3. `frontend/src/lib/components/` — feature components that may be adaptable +4. Inline patterns in existing pages (`frontend/src/routes/`) — badges, skeletons, empty states, collapsibles + +**For each found component, the scan MUST return:** +- Exact file path +- Props interface (what it accepts) +- Whether it's a direct fit, adaptable, or pattern-only + +**Reuse decision tree:** +| Situation | Action | +|-----------|--------| +| Component exists and fits | `@RELATION DEPENDS_ON -> [ExistingComponent]` — zero new code | +| Pattern exists (badge, skeleton, tooltip) | Document the Tailwind classes to replicate; no component extraction | +| No reusable asset exists | Create new component only then | + +**Output:** The `contracts/modules.md` for every frontend contract MUST include `@RELATION` edges to reused components and a `@RATIONALE` noting WHY the component is reused rather than rebuilt. For pattern-only reuse, the contract MUST reference the source page/file where the pattern was observed. + +**Forbidden patterns:** +- Creating a new `` when `confirm()` suffices +- Building a custom `` exists; never schedule "create a toast system" when `addToast()` is wired. + ### Test Tasks Tests are optional only when the feature truly has no new verification surface. Test tasks are usually expected for: diff --git a/.opencode/skills/semantics-svelte/SKILL.md b/.opencode/skills/semantics-svelte/SKILL.md index 216bf94e..090de96a 100644 --- a/.opencode/skills/semantics-svelte/SKILL.md +++ b/.opencode/skills/semantics-svelte/SKILL.md @@ -28,6 +28,12 @@ You are bound by strict repository-level design rules: 3. **API Layer:** You MUST use the internal `fetchApi`/`requestApi` wrappers from `$lib/api`. Using native `fetch()` is a fatal violation. 4. **SvelteKit Routing:** Pages live under `src/routes/`. Components live under `src/lib/components/`. Stores under `src/lib/stores/`. 5. **Testing:** Use Vitest with `@testing-library/svelte` for component tests. +6. **Component Reuse:** Before creating any new component, scan the existing library: + - **Atoms:** `$lib/ui/Button.svelte`, `$lib/ui/Select.svelte`, `$lib/ui/Input.svelte`, `$lib/ui/Card.svelte` + - **Widgets:** `$lib/components/ui/SearchableMultiSelect.svelte`, `$lib/components/ui/MultiSelect.svelte` + - **Infrastructure:** `addToast()` from `$lib/toasts.js` (Toast already mounted in root layout) + - **Patterns (no component needed):** badges (`rounded-full px-2.5 py-0.5 text-xs font-medium`), tooltips (native `title`), skeletons (`animate-pulse bg-gray-200`), collapsibles (`
`), empty states (`border-dashed bg-gray-50`), confirmations (`confirm()`) + Refer to `.opencode/command/speckit.plan.md` §"Frontend Component Reuse Scan" for the mandatory scan workflow. ## II. UX CONTRACTS (STRICT UI BEHAVIOR) diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index a4670ff4..36b3146d 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,50 +1,79 @@ -# [PROJECT_NAME] Constitution - +# ss-tools Constitution + +Конституция не дублирует правила — она объясняет, **почему** каждый принцип важен, и указывает, **где** искать полные инструкции. ## Core Principles -### [PRINCIPLE_1_NAME] - -[PRINCIPLE_1_DESCRIPTION] - +### I. Semantic Contract First -### [PRINCIPLE_2_NAME] - -[PRINCIPLE_2_DESCRIPTION] - +Каждая единица кода (функция, класс, модуль, компонент) должна быть аннотирована GRACE-Poly контрактом. Без контракта код невидим для семантического индекса, непроверяем агентом и недоступен для impact analysis. -### [PRINCIPLE_3_NAME] - -[PRINCIPLE_3_DESCRIPTION] - +**Немедленные правила** → [ADR-0002](docs/adr/ADR-0002-semantic-protocol.md) +**Синтаксис и уровни сложности** → `skill({name="semantics-core"})` +**Методология контрактов** → `skill({name="semantics-contracts"})` -### [PRINCIPLE_4_NAME] - -[PRINCIPLE_4_DESCRIPTION] - +### II. Decision Memory -### [PRINCIPLE_5_NAME] - -[PRINCIPLE_5_DESCRIPTION] - +Каждый архитектурный выбор, отвергающий альтернативу, должен быть записан: `@RATIONALE` (почему выбран этот путь) и `@REJECTED` (что запрещено и почему). Без этого агенты переоткрывают уже исследованные тупики, а долгоживущие сессии накапливают невидимый архитектурный дрейф. -## [SECTION_2_NAME] - +**Протокол ADR** → `skill({name="semantics-contracts"})` §I +**Каталог решений** → [`docs/adr/`](docs/adr/) +**Правило запрета воскрешения**: silently reintroducing `@REJECTED` pattern = fatal regression. Требуется ``. -[SECTION_2_CONTENT] - +### III. External Orchestrator -## [SECTION_3_NAME] - +ss-tools — внешний оркестратор над Apache Superset, а не плагин внутри него. Это даёт: независимый релизный цикл, отсутствие связанности с миграциями Superset, изоляцию DevOps-привилегий от BI-привилегий. -[SECTION_3_CONTENT] - +**Полное обоснование** → [ADR-0003](docs/adr/ADR-0003-orchestrator-pattern.md) + +### IV. Module Discipline + +Файл >400 строк или функция с цикломатической сложностью >10 — сигнал к декомпозиции. Каноническая структура директорий исключает циклические импорты и путаницу агентов при длинных speckit-сессиях. + +**Структура и границы** → [ADR-0001](docs/adr/ADR-0001-module-layout.md) + +### V. RBAC Enforcement + +Все мутирующие операции требуют явной проверки роли. DevOps-привилегии (деплой, миграция, maintenance) отделены от BI-привилегий (просмотр дашбордов). Default-allow запрещён. + +**Модель ролей и паттерны** → [ADR-0005](docs/adr/ADR-0005-auth-rbac.md) + +### VI. Frontend — Svelte 5 Runes Only + +Только runes-синтаксис (`$state`, `$derived`, `$effect`, `$props`). Устаревший синтаксис Svelte 4 создаёт путаницу и баги реактивности. `fromStore` + несколько `$derived` вызывают бесконечный reactive flush loop — задокументированный отказ. + +**Архитектура фронтенда** → [ADR-0006](docs/adr/ADR-0006-frontend-architecture.md) +**Запрет fromStore+$derived** → [ADR-0007](docs/adr/ADR-0007-rejected-fromStore-derived.md) +**Паттерны компонентов** → `skill({name="semantics-svelte"})` + +### VII. Test-Driven for C3+ Contracts + +Контракты уровня C3 и выше требуют тестов, написанных до реализации. Тесты верифицируют `@PRE`/`@POST`/`@INVARIANT`, а не детали реализации. Минимум один тест должен явно проверять, что `@REJECTED` путь производит ожидаемый отказ. + +**Методология тестирования** → `skill({name="semantics-testing"})` + +## Development Workflow + +```text +/speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.implement +``` + +- Ни один этап не пропускается при наличии `[NEEDS CLARIFICATION]` +- Мутация контрактов: preview → apply, никогда сразу apply +- Все артефакты фичи — в `specs//`, никогда в `.kilo/` или `.ai/` + +## Verification Gates + +| Gate | Команда | +|------|---------| +| Backend tests | `cd backend && source .venv/bin/activate && python -m pytest -v` | +| Frontend tests | `cd frontend && npm run test` | +| Backend lint | `cd backend && python -m ruff check .` | +| Frontend lint | `cd frontend && npm run lint` | +| Semantic audit | `axiom_semantic_validation audit_contracts` | ## Governance - -[GOVERNANCE_RULES] - +Конституция имеет приоритет над всеми остальными практиками разработки. Изменения требуют: документирования предложения, проверки на согласованность со всеми ADR, плана миграции затронутого кода, и bump версии. -**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] - +**Version**: 1.0.0 | **Ratified**: 2026-05-22 | **Last Amended**: 2026-05-22 diff --git a/specs/031-maintenance-banner/contracts/modules.md b/specs/031-maintenance-banner/contracts/modules.md index 77a0f37e..b99cf6e5 100644 --- a/specs/031-maintenance-banner/contracts/modules.md +++ b/specs/031-maintenance-banner/contracts/modules.md @@ -13,17 +13,27 @@ @FILE backend/src/models/maintenance.py @RELATION DEPENDS_ON -> [Base:Model] -Columns: id (str PK), task_id (str, FK→tasks.id), tables (JSON), start_time (datetime tz), end_time (datetime tz), message (text?), status (enum: active/completed/failed), created_at, updated_at. +Columns: id (str PK), task_id (str, loose reference to TaskManager), environment_id (str — snapshot of target env at creation), tables (JSON), start_time (datetime tz, required), end_time (datetime tz, nullable), message (text?), status (enum: pending/applying/active/partial/ending/completed/failed), created_at, updated_at. + +### [DEF:MaintenanceDashboardBanner:Model] +@COMPLEXITY 1 +@PURPOSE Canonical "one chart per dashboard" entity — enforces invariant exactly one banner chart per dashboard regardless of active event count. Unique partial index on (environment_id, dashboard_id) WHERE status='active'. +@LAYER models +@FILE backend/src/models/maintenance.py +@RELATION DEPENDS_ON -> [Base:Model] + +Columns: id (str PK), environment_id (str), dashboard_id (int), chart_id (int? — Superset markdown chart ID), banner_text (text — current aggregated markdown), status (enum: active/removed), created_at, updated_at. ### [DEF:MaintenanceDashboardState:Model] @COMPLEXITY 1 -@PURPOSE Junction model linking maintenance events to dashboards with chart tracking. +@PURPOSE Junction linking events to dashboards. References shared MaintenanceDashboardBanner — does not own chart_id directly. @LAYER models @FILE backend/src/models/maintenance.py @RELATION DEPENDS_ON -> [Base:Model] @RELATION DEPENDS_ON -> [MaintenanceEvent:Model] +@RELATION DEPENDS_ON -> [MaintenanceDashboardBanner:Model] -Columns: id (str PK), event_id (str FK→maintenance_events.id), dashboard_id (int), environment_id (str), chart_id (int?), status (enum: active/removed/removal_failed), created_at, removed_at. Index: (dashboard_id, status). +Columns: id (str PK), event_id (str FK→maintenance_events.id), dashboard_id (int), banner_id (str FK→maintenance_dashboard_banners.id), status (enum: pending_apply/active/removing/removed/apply_failed/removal_failed), created_at, updated_at. Index: (dashboard_id, status). ### [DEF:MaintenanceSettings:Model] @COMPLEXITY 1 @@ -32,7 +42,7 @@ Columns: id (str PK), event_id (str FK→maintenance_events.id), dashboard_id (i @FILE backend/src/models/maintenance.py @RELATION DEPENDS_ON -> [Base:Model] -Columns: id (str PK, "default"), target_environment_id (str), dashboard_scope (enum: published_only/draft_only/all), excluded_dashboard_ids (JSON), forced_dashboard_ids (JSON), updated_at. +Columns: id (str PK, "default"), target_environment_id (str), display_timezone (str, default "UTC"), banner_template (text — markdown with optional `{start_time}`, `{end_time}`, `{message}`; default includes all three), dashboard_scope (enum: published_only/draft_only/all), excluded_dashboard_ids (JSON), forced_dashboard_ids (JSON), updated_at. --- @@ -43,48 +53,31 @@ Columns: id (str PK, "default"), target_environment_id (str), dashboard_scope (e @FILE backend/src/api/routes/maintenance/_schemas.py @RELATION DEPENDS_ON -> [pydantic.BaseModel] -Schemas: MaintenanceStartRequest, MaintenanceSettingsUpdate, MaintenanceStartResponse, MaintenanceTaskResult, MaintenanceEndResponse, MaintenanceEndTaskResult, MaintenanceEndAllTaskResult, MaintenanceSettingsResponse, MaintenanceEventItem, MaintenanceEventListResponse, MaintenanceDashboardBannerState. - ---- - ### [DEF:SupersetDashboardsWriteMixin:Class] @COMPLEXITY 4 -@PURPOSE Extends SupersetClient with write operations: create markdown chart, update dashboard layout, delete chart. +@PURPOSE Extends SupersetClient with dashboard write operations. @LAYER core @FILE backend/src/core/superset_client/_dashboards_write.py @RELATION DEPENDS_ON -> [SupersetClientBase:Class] @RELATION DEPENDS_ON -> [NetworkClient:Class] @PRE SupersetClient is initialized with valid bearer token and environment base URL. -@POST Chart created in Superset; chart_id returned. Dashboard position_json updated. -@SIDE_EFFECT Modifies Superset dashboards via REST API (POST /chart/, PUT /dashboard/{id}, DELETE /chart/{id}). -@RATIONALE Direct chart API is faster than export-import cycle for single-chart operations (see research.md R1). +@POST Chart created/updated/deleted in Superset. Dashboard position_json updated. +@SIDE_EFFECT Modifies Superset dashboards via REST API. +@RATIONALE Direct chart API faster than export-import cycle for single-chart ops. -Methods: -- `create_markdown_chart(dashboard_id: int, markdown_text: str) -> int` — Creates viz_type="markdown" chart, returns chart_id. -- `update_dashboard_layout(dashboard_id: int, chart_id: int, position: str) -> dict` — Patches position_json to insert chart at given position. -- `delete_chart(chart_id: int) -> bool` — Deletes a chart from Superset. -- `get_dashboard_layout(dashboard_id: int) -> dict` — Fetches current position_json for reading existing layout. - ---- +Methods: `create_markdown_chart`, `update_markdown_chart`, `update_dashboard_layout` (insert at (0,0), 12 cols, shift existing down), `remove_chart_from_layout`, `delete_chart`, `get_dashboard_layout`. ### [DEF:SqlTableExtractor:Module] @COMPLEXITY 2 -@PURPOSE Extract fully-qualified table names (schema.table) from Superset virtual dataset SQL+Jinja text. Uses two-phase approach: (1) regex global extraction of all schema.table candidates from raw text, (2) sqlparse filtering to reject candidates inside SQL string literals. Jinja is NOT pre-stripped — table names inside {% set %} blocks are preserved. +@PURPOSE Extract schema.table from virtual dataset SQL+Jinja. Two-phase: Jinja span detection, regex global extraction, sqlparse filtering only on SQL spans (string literals). @LAYER services @FILE backend/src/services/sql_table_extractor.py @RELATION DEPENDS_ON -> [sqlparse] -@RATIONALE Table names inside Jinja {% set %} blocks (common in Superset virtual datasets) are invisible to SQL parsers after Jinja stripping. Global extraction first, then false-positive filtering, ensures full coverage. See research.md R3. - -Functions: -- `extract_tables_from_sql(raw_sql: str) -> set[str]` — Orchestrator: calls extract_candidates then filter_string_literals. Returns deduplicated set of "schema.table" names (case-insensitive matching per clarification Q3). -- `extract_table_candidates(raw_sql: str) -> list[str]` — Regex finds all `[a-zA-Z][\w]*\.[a-zA-Z][\w]*` patterns across raw text (SQL + Jinja). -- `filter_string_literals(raw_sql: str, candidates: list[str]) -> set[str]` — Uses sqlparse tokenizer to reject candidates whose byte position falls inside a SQL string literal or comment. - ---- +@RATIONALE Table names in {% set %} blocks must survive; stripping Jinja first loses them. ### [DEF:MaintenanceService:Class] @COMPLEXITY 4 -@PURPOSE Core business logic: find affected dashboards, apply banners, remove banners, rebuild aggregated banner text. +@PURPOSE Core business logic: discovery, banner placement/removal, text rebuild. @LAYER services @FILE backend/src/services/maintenance_service.py @RELATION DEPENDS_ON -> [SupersetClient:Class] @@ -92,26 +85,19 @@ Functions: @RELATION DEPENDS_ON -> [SqlTableExtractor:Module] @RELATION DEPENDS_ON -> [MaintenanceEvent:Model] @RELATION DEPENDS_ON -> [MaintenanceDashboardState:Model] +@RELATION DEPENDS_ON -> [MaintenanceDashboardBanner:Model] @RELATION DEPENDS_ON -> [MaintenanceSettings:Model] @RELATION DEPENDS_ON -> [TaskManager:Class] -@PRE Active database session available. Superset client initialized for target environment. -@POST MaintenanceEvent created/updated. MaintenanceDashboardState records created/updated. Chart added/removed from Superset dashboards. -@SIDE_EFFECT Creates/deletes charts in Superset; writes to maintenance_events and maintenance_dashboard_states tables; dispatches WebSocket task progress events. -@RATIONALE Single service orchestrates the full maintenance lifecycle, ensuring consistency between local state and Superset state (see research.md R2). +@PRE Active DB session. Superset client initialized. +@POST Event/state/banner records updated. Charts created/updated/deleted. +@SIDE_EFFECT Superset mutations; DB writes; WebSocket progress events. +@RATIONALE Single orchestrator ensures consistency between local state and Superset. -Methods: -- `start_maintenance(tables, start_time, end_time, message, db_session) -> MaintenanceEvent` — C4: Discovery + banner placement. -- `end_maintenance(event_id, db_session) -> None` — C4: Banner removal for one event. -- `end_all_maintenance(db_session) -> None` — C4: Bulk banner removal. -- `find_affected_dashboards(tables, superset_client) -> list[dict]` — C3: Cross-references tables against datasets. -- `build_banner_text(events: list[MaintenanceEvent]) -> str` — C2: Aggregates text from multiple active events. -- `rebuild_banner(dashboard_id, superset_client, db_session) -> None` — C3: Updates chart markdown text in Superset. - ---- +Methods: `start_maintenance` (idempotency by (tables, start, end)), `end_maintenance`, `end_all_maintenance`, `find_affected_dashboards`, `ensure_banner_chart` (shared chart per dashboard), `build_banner_text` (template substitution), `rebuild_banner` (update_markdown_chart). ### [DEF:MaintenanceRoutes:Module] @COMPLEXITY 3 -@PURPOSE FastAPI route handlers for maintenance API endpoints. +@PURPOSE FastAPI route handlers for maintenance endpoints. @LAYER api @FILE backend/src/api/routes/maintenance/_routes.py @RELATION DEPENDS_ON -> [MaintenanceService:Class] @@ -119,117 +105,92 @@ Methods: @RELATION DEPENDS_ON -> [TaskManager:Class] @RELATION DEPENDS_ON -> [AuthMiddleware:Class] -Endpoints: -- `POST /api/maintenance/start` → MaintenanceStartResponse -- `POST /api/maintenance/{maintenance_id}/end` → MaintenanceEndResponse -- `POST /api/maintenance/end-all` → MaintenanceEndResponse -- `GET /api/maintenance/events` → MaintenanceEventListResponse -- `GET /api/maintenance/settings` → MaintenanceSettingsResponse -- `PUT /api/maintenance/settings` → MaintenanceSettingsResponse -- `GET /api/maintenance/dashboard-banners` → list[MaintenanceDashboardBannerState] - ---- +7 endpoints: start (always 202), end (202), end-all (202), events (GET), settings (GET/PUT), dashboard-banners (GET). RBAC per FR-015 matrix. ### [DEF:MaintenanceRouter:Module] @COMPLEXITY 2 -@PURPOSE APIRouter assembly with prefix and tag registration. +@PURPOSE APIRouter assembly with prefix and tag. @LAYER api @FILE backend/src/api/routes/maintenance/_router.py @RELATION DEPENDS_ON -> [MaintenanceRoutes:Module] -Creates `APIRouter(prefix="/api/maintenance", tags=["maintenance"])` and includes all route handlers. +--- ## Frontend Contracts ### [DEF:MaintenanceStore:Module] @COMPLEXITY 3 -@PURPOSE Svelte 5 rune store for maintenance event state, shared across components. +@PURPOSE Svelte 5 rune store for maintenance state. Shared by management page and Dashboard Hub badge. @LAYER store @FILE frontend/src/lib/stores/maintenance.svelte.js @RELATION DEPENDS_ON -> [requestApi:Function] -@RATIONALE Shared store enables maintenance badge in Dashboard Hub and management page to share state without prop drilling (ADR-0006). +@RATIONALE Shared store avoids prop drilling across routes. Uses `$effect(subscribe)` per ADR-0007. -State: -- `events`: $state — active and completed event lists. -- `settings`: $state — MaintenanceSettings object. -- `dashboardBanners`: $state — Map for Dashboard Hub indicator. -- `isLoading`: $state — loading flag. -- `error`: $state — error message. +State: `events` ($state), `settings` ($state), `dashboardBanners` ($state Map), `isLoading`, `error`. Auto-polls events every 30s; WebSocket covers task progress. ### [DEF:MaintenanceApiClient:Module] @COMPLEXITY 2 -@PURPOSE Typed fetch wrappers for all maintenance API endpoints. +@PURPOSE Typed fetch wrappers for maintenance API. @LAYER api @FILE frontend/src/lib/api/maintenance.js @RELATION DEPENDS_ON -> [requestApi:Function] -Functions: -- `startMaintenance(payload)` → `{task_id, maintenance_id}` -- `endMaintenance(maintenanceId)` → `{task_id}` -- `endAllMaintenance()` → `{task_id}` -- `getEvents()` → `{active, completed}` -- `getSettings()` → `MaintenanceSettingsResponse` -- `updateSettings(payload)` → `MaintenanceSettingsResponse` -- `getDashboardBanners()` → `Map` - ---- +7 functions mirroring the 7 API endpoints. ### [DEF:MaintenanceBannerPage:Component] @COMPLEXITY 3 -@PURPOSE Management page: settings panel + active/completed events tables + bulk actions. +@PURPOSE Management page at `/maintenance`. Compose settings panel + events table from store. @LAYER route @FILE frontend/src/routes/maintenance/+page.svelte @RELATION DEPENDS_ON -> [MaintenanceStore:Module] @RELATION DEPENDS_ON -> [MaintenanceSettingsPanel:Component] @RELATION DEPENDS_ON -> [MaintenanceEventsTable:Component] -@UX_STATE idle -> Page loaded with settings and events tables. -@UX_STATE loading -> Skeleton placeholders for both panels. -@UX_STATE action_in_progress -> Remove button shows spinner; other actions disabled. -@UX_STATE error -> Error toast with retry button. -@UX_STATE empty -> "No active maintenance events" placeholder. -@UX_FEEDBACK Toast on success (green), error (red), warning (yellow partial). -@UX_RECOVERY Retry button on error. Auto-polling resumes after recovery. -@UX_REACTIVITY $effect polls MaintenanceStore every 30s. - ---- +@UX_STATE idle/loading/action_in_progress/error/empty +@UX_FEEDBACK Toast via `addToast()` from `$lib/toasts.js` ### [DEF:MaintenanceSettingsPanel:Component] @COMPLEXITY 3 -@PURPOSE Collapsible settings form: dashboard scope radio, excluded/forced dashboard multi-select. +@PURPOSE Collapsible settings form using existing UI primitives. @LAYER component @FILE frontend/src/lib/components/MaintenanceSettingsPanel.svelte @RELATION DEPENDS_ON -> [MaintenanceStore:Module] -@UX_STATE idle -> Form with current values. -@UX_STATE saving -> Save button disabled, spinner. -@UX_STATE saved -> Brief green flash on form border. -@UX_STATE error -> Red border, error message below form. -@UX_FEEDBACK Toast on save success/failure. -@UX_RECOVERY Retry save on error. +@RELATION DEPENDS_ON -> [Select:Component] (from `$lib/ui/Select.svelte`) +@RELATION DEPENDS_ON -> [Input:Component] (from `$lib/ui/Input.svelte`) +@RELATION DEPENDS_ON -> [SearchableMultiSelect:Component] (from `$lib/components/ui/SearchableMultiSelect.svelte`) +@RATIONALE Reuses existing form atoms (Select, Input) and dashboard picker (SearchableMultiSelect) — zero new UI primitives needed. ---- +Form fields: +- Environment: `` text +- Scope: native `` (matches ScheduleConfig pattern) +- Excluded/Forced: `` with dashboards as `{id, name}` +- Banner template: `