# Tasks: Maintenance Banner for Dashboards

**Input**: Design documents from `specs/031-maintenance-banner/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/modules.md ✅

**Tests**: Included for all C3+ contracts, new API endpoints, database models, and ADR-rejected regression paths.

**Organization**: Tasks grouped by user story for independent implementation and verification.

## Format: `- [ ] T### [P?] [USx] Description with exact file path`

- **[P]**: Can run in parallel (different files, no dependencies)
- **[USx]**: Which user story (US1-US5); `--` for setup/foundational/polish phases
- Paths relative to repository root

---

## Phase 1: Setup (Shared Infrastructure)

**Purpose**: Project scaffolding — directories, package inits, Alembic migration skeleton.

- [ ] T001 Create `backend/src/api/routes/maintenance/` package directory structure (`__init__.py`)
- [ ] T002 [P] Create `backend/tests/` test file stubs for maintenance feature: `test_maintenance_service.py`, `test_sql_table_extractor.py`, `test_maintenance_api.py`
- [ ] T003 [P] Create frontend directory structure: `frontend/src/routes/maintenance/`, `frontend/src/lib/stores/maintenance.svelte.js`, `frontend/src/lib/api/maintenance.js` (empty placeholder files). Note: UI components reuse existing primitives — no new component scaffolds needed.
- [ ] T004 Generate Alembic migration for new tables (`maintenance_events`, `maintenance_dashboard_states`, `maintenance_settings`) via `alembic revision --autogenerate -m "add maintenance banner tables"` after models are defined in Phase 2

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented. All models, schemas, SupersetClient extension, SQL extractor, router scaffold.

**⚠️ CRITICAL**: No user story work can begin until this phase is complete.

### Database Models

- [ ] T005 [P] [--] Implement `MaintenanceEvent` SQLAlchemy model in `backend/src/models/maintenance.py` with columns (id, task_id — loose reference, environment_id — snapshot of target env, tables JSON, start_time, end_time nullable, message, status enum: pending/applying/active/partial/ending/completed/failed, timestamps) and relationship to MaintenanceDashboardState. Contract: `[DEF:MaintenanceEvent:Model]` C1.
- [ ] T005a [P] [--] Implement `MaintenanceDashboardBanner` SQLAlchemy model in `backend/src/models/maintenance.py` — canonical "one chart per dashboard" entity with columns (id, environment_id, dashboard_id, chart_id, banner_text, status enum: active/removed, timestamps) and unique partial index on (environment_id, dashboard_id) WHERE status='active'. Contract: `[DEF:MaintenanceDashboardBanner:Model]` C1.
- [ ] T006 [P] [--] Implement `MaintenanceDashboardState` SQLAlchemy model in `backend/src/models/maintenance.py` with columns (id, event_id FK, dashboard_id, banner_id FK→maintenance_dashboard_banners, status enum: pending_apply/active/removing/removed/apply_failed/removal_failed, timestamps) and composite index on (dashboard_id, status). Contract: `[DEF:MaintenanceDashboardState:Model]` C1.
- [ ] T007 [P] [--] Implement `MaintenanceSettings` SQLAlchemy model in `backend/src/models/maintenance.py` — single-row table (id="default") with CheckConstraint(id='default'), columns (target_environment_id, display_timezone default "UTC", banner_template text, dashboard_scope enum, excluded_dashboard_ids JSON, forced_dashboard_ids JSON, updated_at). Contract: `[DEF:MaintenanceSettings:Model]` C1.

### Pydantic Schemas

- [ ] T008 [--] Implement all Pydantic request/response schemas in `backend/src/api/routes/maintenance/_schemas.py`: MaintenanceStartRequest, MaintenanceSettingsUpdate, MaintenanceStartResponse, MaintenanceTaskResult, MaintenanceDashboardResult, MaintenanceFailedDashboard, MaintenanceEndResponse, MaintenanceEndTaskResult, MaintenanceEndAllTaskResult, MaintenanceSettingsResponse, MaintenanceEventItem, MaintenanceEventListResponse, MaintenanceDashboardBannerState. Contract: `[DEF:MaintenanceSchemas:Module]` C1.

### SupersetClient Extension (C4 — REQUIRES belief runtime)

- [ ] T009 [--] Implement `SupersetDashboardsWriteMixin` in `backend/src/core/superset_client/_dashboards_write.py` with methods: `create_markdown_chart(dashboard_id, markdown_text) → chart_id` (POST /api/v1/chart/ with viz_type="markdown"), `update_markdown_chart(chart_id, markdown_text)` (PUT /api/v1/chart/{id} to update markdown params), `update_dashboard_layout(dashboard_id, chart_id)` (insert at position (0,0), full width 12 cols, shift existing components down), `remove_chart_from_layout(dashboard_id, chart_id)` (remove from position_json), `delete_chart(chart_id)` (DELETE /api/v1/chart/{id}), `get_dashboard_layout(dashboard_id)` (GET /api/v1/dashboard/{id} for position_json). Contract: `[DEF:SupersetDashboardsWriteMixin:Class]` C4. (RATIONALE: chart_id tracked in MaintenanceDashboardBanner DB table — no need to scan position_json)
- [ ] T010 [--] Instrument `SupersetDashboardsWriteMixin` with belief runtime markers: `belief_scope` context manager wrapping each Superset API call, `log(REASON)` before POST/PUT/DELETE, `log(REFLECT)` on success, `log(EXPLORE)` on HTTP errors.

### SQL Table Extractor (C2)

- [ ] T011 [--] Implement `SqlTableExtractor` module in `backend/src/services/sql_table_extractor.py` with two-phase extraction: (1) determine Jinja block spans vs SQL spans, (2) in Jinja spans — extract `"schema.table"` from string values, (3) in SQL spans — regex `[a-zA-Z][\w]*\.[a-zA-Z][\w]*` + sqlparse filtering to reject SQL string literal/comment positions. Orchestrator: `extract_tables_from_sql(raw_sql) → set[str]`. Contract: `[DEF:SqlTableExtractor:Module]` C2. (RATIONALE: Jinja string values in {% set %} blocks must NOT be filtered as SQL strings; REJECTED: sqlparse-only approach loses Jinja-embedded tables)

### Router & App Registration

- [ ] T012 [--] Implement `MaintenanceRouter` in `backend/src/api/routes/maintenance/_router.py` — creates `APIRouter(prefix="/api/maintenance", tags=["maintenance"])`. Contract: `[DEF:MaintenanceRouter:Module]` C2.
- [ ] T013 [--] Implement route handler stubs in `backend/src/api/routes/maintenance/_routes.py` — all 7 endpoint signatures with placeholder returns and proper RBAC `Depends(require_role(...))` guards. Contract: `[DEF:MaintenanceRoutes:Module]` C3 (stub phase).
- [ ] T014 [--] Register `maintenance_router` in main FastAPI application (`backend/src/app.py`) and register `SupersetDashboardsWriteMixin` in SupersetClient assembly.

### Seed Data

- [ ] T015 [--] Create Alembic data migration (or startup check) to insert default `MaintenanceSettings` row (id="default", target_environment_id=<from config>, dashboard_scope="published_only", excluded=[], forced=[]) if not exists.

**Checkpoint**: Foundation ready — models, schemas, SupersetClient extension, SQL extractor, router, and seed data in place. All user stories can now begin.

---

## Phase 3: User Story 1 — Banner Placement (Priority: P1) 🎯 MVP

**Goal**: External tool calls `POST /api/maintenance/start` → ss-tools finds affected dashboards → places markdown banners.

**Independent Test**: Call API with a known table name, verify banner appears on affected dashboards in Superset.

### Tests for US1 (Write FIRST, ensure they FAIL)

- [ ] T016 [P] [US1] Write contract test for `POST /api/maintenance/start` in `backend/tests/test_maintenance_api.py` — test valid request returns `202`, test missing `start_time` returns 422, test `end_time < start_time` returns 422, test past `start_time` >1h returns 422, test `tables` >100 entries returns 422, test optional `end_time` (omitted → task result shows «уточняется»), test duplicate request returns `200 {status:"already_active"}`, test no-match in task result, test RBAC returns 403, test HTML injection in message returns 400.
- [ ] T017 [P] [US1] Write unit test for `SqlTableExtractor.extract_tables_from_sql()` in `backend/tests/test_sql_table_extractor.py` — test table-based matching, virtual-SQL extraction with Jinja `{% set %}`, string literal false-positive rejection, case-insensitive matching.
- [ ] T018 [P] [US1] Write integration test for `MaintenanceService.start_maintenance()` in `backend/tests/test_maintenance_service.py` — test dashboard discovery, markdown chart creation, MaintenanceEvent persistence, MaintenanceDashboardState creation, idempotency check.

### Implementation for US1

- [ ] T019 [US1] Implement `find_affected_dashboards()` method in `backend/src/services/maintenance_service.py` — fetches all datasets from Superset, matches table-based datasets via (schema, table_name), matches virtual datasets via SqlTableExtractor, applies scope/excluded/forced filtering from MaintenanceSettings, returns deduplicated dashboard list. Contract C3 within `[DEF:MaintenanceService:Class]`.
- [ ] T019a [US1] Implement `ensure_banner_chart()` method in `backend/src/services/maintenance_service.py` — idempotently gets or creates the shared `MaintenanceDashboardBanner` for a dashboard. If banner exists (ACTIVE in DB) → return it. Otherwise → `create_markdown_chart()` + `update_dashboard_layout()`, store chart_id in banner row. Contract C3. (RATIONALE: DB is source of truth for chart existence — no position_json scanning needed; REJECTED: per-event chart creation)
- [ ] T020 [US1] Implement `build_banner_text()` method in `backend/src/services/maintenance_service.py` — reads `banner_template` and `display_timezone` from MaintenanceSettings, substitutes `{start_time}`, `{end_time}`, `{message}` variables. Missing variables in template are simply omitted. Single-event: direct substitution. Multi-event: aggregates per-event start/end/message, then substitutes into template. Handles absent `end_time` → «Окончание: уточняется». Contract C2.
- [ ] T021 [US1] Implement `start_maintenance()` method in `backend/src/services/maintenance_service.py` — orchestrates full flow: (1) idempotency check by `(tables, start_time, end_time)` key using row-level lock (`SELECT ... FOR UPDATE` on maintenance_events to prevent race condition from concurrent identical /start calls), (2) create MaintenanceEvent (status=PENDING→APPLYING), (3) `find_affected_dashboards()`, (4) for each dashboard: `ensure_banner_chart()` → create MaintenanceDashboardState (PENDING_APPLY→ACTIVE or APPLY_FAILED), rebuild aggregated banner text via `build_banner_text()` + `update_markdown_chart()`, (5) transition event to ACTIVE (all success) or PARTIAL (some failed). Contract C4.
- [ ] T022 [US1] Instrument `start_maintenance()` with belief runtime: `belief_scope("start_maintenance")`, `log(REASON)` at entry/checkpoint, `log(REFLECT)` on completion, `log(EXPLORE)` on Superset errors.
- [ ] T023 [US1] Implement `POST /api/maintenance/start` route handler in `backend/src/api/routes/maintenance/_routes.py` — validates MaintenanceStartRequest: `start_time` required, `end_time` optional (absent → banner text shows «Окончание: уточняется»), message field sanitized (reject raw HTML, escape special chars). Idempotency check by `(tables, start_time, end_time)` key. Otherwise: create MaintenanceEvent, dispatch task via TaskManager, return `202 {task_id, maintenance_id, status:"pending"}`. RBAC: `Depends(require_role("operator"))`.
- [ ] T024 [US1] Run backend verification: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_maintenance_api.py backend/tests/test_maintenance_service.py backend/tests/test_sql_table_extractor.py -v`

**Checkpoint**: `POST /api/maintenance/start` fully functional. Banners placed on affected dashboards. Idempotency working.

---

## Phase 4: User Story 2 — Banner Removal (Priority: P1)

**Goal**: External tool calls `POST /api/maintenance/{id}/end` or `/end-all` → banners removed from dashboards.

**Independent Test**: After US1, call /end and verify banners disappear; call again and verify idempotent.

### Tests for US2 (Write FIRST, ensure they FAIL)

- [ ] T025 [P] [US2] Write contract test for `POST /api/maintenance/{id}/end` in `backend/tests/test_maintenance_api.py` — test successful removal, test already-completed idempotent response, test multi-event scenario (banner stays when other active events exist, text rebuilt).
- [ ] T026 [P] [US2] Write integration test for `MaintenanceService.end_maintenance()` in `backend/tests/test_maintenance_service.py` — test chart deletion via Superset API, test MaintenanceDashboardState transition to REMOVED, test MaintenanceEvent transition to COMPLETED, test banner rebuild when other active events remain.
- [ ] T026a [P] [US2] Write integration test for `MaintenanceService.end_all_maintenance()` in `backend/tests/test_maintenance_service.py` — test bulk closure of all active events, verify each transitions to COMPLETED, test mixed state (some dashboards have other active events → banner stays, others → banner removed).

### Implementation for US2

- [ ] T027 [US2] Implement `rebuild_banner()` method in `backend/src/services/maintenance_service.py` — for a given `MaintenanceDashboardBanner`, queries all active `MaintenanceDashboardState` records linked to it, calls `build_banner_text()` for aggregated text, updates the shared markdown chart in Superset via `SupersetDashboardsWriteMixin.update_markdown_chart()`. Contract C3 within `[DEF:MaintenanceService:Class]`.
- [ ] T028 [US2] Implement `end_maintenance()` method in `backend/src/services/maintenance_service.py` — set event status to ENDING, then for each active `MaintenanceDashboardState` in the event: (1) check if OTHER active event-states still reference the same banner; (2) if yes → call `rebuild_banner()` (remove this event's info from aggregated text), transition state to REMOVED; (3) if no → transition banner to REMOVED, call `remove_chart_from_layout()` then `delete_chart()`, transition state to REMOVING→REMOVED (or REMOVAL_FAILED on error); (4) transition event to COMPLETED. Contract C4. (RATIONALE: conditional removal prevents banner flicker; REJECTED: unconditional removal)
- [ ] T029 [US2] Instrument `end_maintenance()` with belief runtime markers.
- [ ] T030 [US2] Implement `end_all_maintenance()` method in `backend/src/services/maintenance_service.py` — iterates all active events, calls `end_maintenance()` for each. Contract C4.
- [ ] T031 [US2] Implement `POST /api/maintenance/{maintenance_id}/end` and `POST /api/maintenance/end-all` route handlers in `backend/src/api/routes/maintenance/_routes.py` — dispatch tasks via TaskManager, return task_id immediately. RBAC: `Depends(require_role("operator"))`.
- [ ] T032 [US2] Run backend verification: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_maintenance_api.py backend/tests/test_maintenance_service.py -v -k "end"`

**Checkpoint**: Banner removal fully functional. Multi-event conflict handling verified. Idempotent /end confirmed.

---

## Phase 5: User Story 4 — Settings Configuration (Priority: P3)

**Goal**: Administrator can configure maintenance scope (published/draft/all, excluded dashboards, forced dashboards) via API and UI.

**Note**: Settings model is already in Phase 2 (T007). This phase adds API endpoints and UI panel.

### Tests for US4

- [ ] T033 [P] [US4] Write API test for `GET /api/maintenance/settings` and `PUT /api/maintenance/settings` in `backend/tests/test_maintenance_api.py` — test read returns defaults, test update with valid payload, test RBAC (admin only for PUT), test invalid scope rejection.

### Implementation for US4

- [ ] T034 [US4] Implement `GET /api/maintenance/settings` and `PUT /api/maintenance/settings` route handlers in `backend/src/api/routes/maintenance/_routes.py`. GET: read MaintenanceSettings row. PUT: validate and update fields (target_environment_id, display_timezone, banner_template, dashboard_scope, excluded, forced). Return updated settings. RBAC: GET = `operator+`, PUT = `admin`.
- [ ] T035 [P] [US4] Implement `MaintenanceApiClient` module in `frontend/src/lib/api/maintenance.js` — typed fetch wrappers: `getSettings()`, `updateSettings(payload)`, `getEvents()`, `startMaintenance(payload)`, `endMaintenance(id)`, `endAllMaintenance()`, `getDashboardBanners()`. Uses `requestApi` from existing API layer. Contract: `[DEF:MaintenanceApiClient:Module]` C2.
- [ ] T036 [US4] Implement `MaintenanceSettingsPanel` component in `frontend/src/lib/components/MaintenanceSettingsPanel.svelte` — collapsible via `<details><summary>`. Form uses: `<Select>` from `$lib/ui/Select.svelte` for target environment, `<Input>` from `$lib/ui/Input.svelte` for timezone, native `<input type="radio" bind:group>` for scope (matches ScheduleConfig pattern), `<SearchableMultiSelect>` from `$lib/components/ui/SearchableMultiSelect.svelte` for excluded/forced dashboards (options: `{id, name}` from dashboard list), `<textarea>` for banner_template with live preview, `<Button isLoading={isSaving}>` from `$lib/ui/Button.svelte` for save. Toast via `addToast()` from `$lib/toasts.js`. UX states: idle/saving/saved/error. Contract C3.
- [ ] T037 [US4] Run verification: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_maintenance_api.py -v -k "settings"` + `cd frontend && npx vitest run tests/maintenance.test.ts -v -k "settings"`

**Checkpoint**: Settings API operational. Settings panel renders in UI.

---

## Phase 6: User Story 3 — Admin UI Management Page (Priority: P2)

**Goal**: Administrator sees active/completed events table with selective and bulk removal actions.

**Independent Test**: Navigate to /maintenance, view active events, click "Remove All" → banners disappear.

### Tests for US3

- [ ] T038 [P] [US3] Write component test for `MaintenanceEventsTable` in `frontend/tests/maintenance.test.ts` — test renders active and completed events, test "Remove Banners" per event, test "Remove All" with confirmation dialog, test error state with retry.
- [ ] T039 [P] [US3] Write store test for `MaintenanceStore` in `frontend/tests/maintenance-store.test.ts` — test events fetching, test polling, test error handling.

### Implementation for US3

- [ ] T040 [US3] Implement `GET /api/maintenance/events` route handler in `backend/src/api/routes/maintenance/_routes.py` — returns MaintenanceEventListResponse with active and completed events, each including affected dashboard count. RBAC: authenticated.
- [ ] T041 [US3] Implement `MaintenanceStore` in `frontend/src/lib/stores/maintenance.svelte.js` — Svelte 5 rune store with `$state` for events (active + completed), settings, dashboardBanners, isLoading, error. Uses `$effect(() => subscribe(...))` pattern per ADR-0007 (REJECTED: fromStore + $derived). Auto-polls `getEvents()` every 30s via `$effect` (WebSocket covers task progress; polling ensures event-list consistency after task completion). Contract: `[DEF:MaintenanceStore:Module]` C3.
- [ ] T042 [US3] Implement `MaintenanceEventsTable` component in `frontend/src/lib/components/MaintenanceEventsTable.svelte` — inline `<table class="min-w-full divide-y divide-gray-200">` (matches health page pattern). Columns: Event ID, Tables (truncated with native `title` tooltip), Affected Dashboards, Start/End Times, Status (inline badge: `rounded-full px-2.5 py-0.5 text-xs font-medium`), Actions (`<Button>` from `$lib/ui/Button.svelte`). Per-event "Remove" button with `isLoading`. "Remove All" with `confirm()` (matches health page). Skeleton: `animate-pulse bg-gray-200 h-4` rows. Empty: `border-dashed border-gray-300 bg-gray-50 p-8 text-center`. Completed events collapsible via `<details><summary>`. Toast via `addToast()`. Contract C3.
- [ ] T043 [US3] Implement `MaintenanceBannerPage` in `frontend/src/routes/maintenance/+page.svelte` — composes `MaintenanceSettingsPanel` + `MaintenanceEventsTable`, passes store data. Uses `addToast()` from `$lib/toasts.js` for feedback (Toast.svelte already mounted in root layout). UX states: idle/loading/action_in_progress/error/empty. Contract C3.
- [ ] T044 [US3] Add navigation entry for `/maintenance` in sidebar — add category to "Operations" section in `frontend/src/lib/components/layout/sidebarNavigation.js` with id="maintenance", path="/maintenance", requiredPermission="maintenance:write", requiredAction="READ" (operator+ see page, admin has full access per RBAC matrix FR-015). Add translation keys to `frontend/src/lib/i18n/locales/en/nav.json` and `frontend/src/lib/i18n/locales/ru/nav.json`.
- [ ] T045 [US3] Run full frontend verification: `cd frontend && npx vitest run tests/maintenance.test.ts tests/maintenance-store.test.ts -v` + `cd frontend && npm run build`

**Checkpoint**: Admin management page fully functional. Settings + events tables operational.

---

## Phase 7: User Story 5 — Dashboard Hub Badge (Priority: P3)

**Goal**: Dashboard Hub table shows orange "⚠️ Maintenance" badge when dashboard has active banner.

**Independent Test**: After US1, open Dashboard Hub → affected dashboard row shows orange badge with tooltip.

### Tests for US5

- [ ] T046 [P] [US5] Write component test for `DashboardMaintenanceBadge` in `frontend/tests/maintenance.test.ts` — test renders badge when maintenance active, test no badge when inactive, test tooltip content, test loading skeleton state.

### Implementation for US5

- [ ] T047 [US5] Implement `GET /api/maintenance/dashboard-banners` route handler in `backend/src/api/routes/maintenance/_routes.py` — returns list of MaintenanceDashboardBannerState: {dashboard_id, active: bool, events: [...], start_time, end_time}. Queries MaintenanceDashboardState where status=ACTIVE. RBAC: authenticated.
- [ ] T048 [US5] Add `getDashboardBanners()` to MaintenanceStore — fetches banner states and populates `dashboardBanners` $state map.
- [ ] T049 [US5] Implement `DashboardMaintenanceBadge` component in `frontend/src/lib/components/DashboardMaintenanceBadge.svelte` — orange badge `rounded-full px-2.5 py-0.5 text-xs font-medium bg-orange-100 text-orange-700 border border-orange-200` (matches existing DashboardHub badge convention). Native `title` attribute for tooltip (codebase-wide pattern). Renders nothing when no active banner. Loading: `animate-pulse bg-gray-200 h-5 w-24 rounded-full`. Contract C2.
- [ ] T050 [US5] Modify `frontend/src/routes/dashboards/+page.svelte` — add `DashboardMaintenanceBadge` import, insert badge column/cell in dashboard table rows, wire `$derived` from MaintenanceStore.dashboardBanners. Ensure compliance with ADR-0007 (no fromStore + $derived pattern). Export visual verification: open Dashboard Hub page after US1 and confirm badge appears via `chrome-devtools` snapshot.
- [ ] T051 [US5] Run verification: `cd frontend && npx vitest run tests/maintenance.test.ts -v -k "badge"` + `cd frontend && npm run build`

**Checkpoint**: Dashboard Hub badge functional. Operators can identify maintenance-affected dashboards at a glance.

---

## Phase 8: Polish & Cross-Cutting Verification

**Purpose**: Full integration verification, linting, semantic audit, ADR compliance check.

- [ ] T052 [P] Run full backend test suite: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_maintenance_*.py backend/tests/test_sql_table_extractor.py -v --cov=src/services/maintenance_service.py --cov=src/services/sql_table_extractor.py --cov=src/api/routes/maintenance/ --cov-report=term-missing`
- [ ] T053 [P] Run backend lint: `cd backend && python -m ruff check src/api/routes/maintenance/ src/services/maintenance_service.py src/services/sql_table_extractor.py src/models/maintenance.py src/core/superset_client/_dashboards_write.py`
- [ ] T054 [P] Run frontend lint: `cd frontend && npm run lint`
- [ ] T055 [P] Run frontend full test suite: `cd frontend && npm run test`
- [ ] T056 [P] Run frontend build check: `cd frontend && npm run build`
- [ ] T057 Run semantic contract audit: `axiom_semantic_validation audit_contracts` on `backend/src/api/routes/maintenance/`, `backend/src/services/maintenance_service.py`, `backend/src/services/sql_table_extractor.py`, `backend/src/core/superset_client/_dashboards_write.py`, `backend/src/models/maintenance.py` — verify no missing C4 tags, no orphan relations, all `@RATIONALE`/`@REJECTED` present.
- [ ] T058 Run ADR rejected-path regression check: verify `fromStore + $derived` NOT used (ADR-0007), Svelte 5 runes syntax only (ADR-0006), no export-import cycle for chart placement (research.md R1 rejected), no per-event chart creation (one-chart-per-dashboard invariant via MaintenanceDashboardBanner unique index), no `plugin:migration` permission in maintenance code (FR-015 RBAC matrix).
- [ ] T059 Run belief runtime audit: `axiom_semantic_validation audit_belief_runtime` on C4 contracts — verify `belief_scope`, `log(REASON)`, `log(REFLECT)`, `log(EXPLORE)` instrumentation present in `start_maintenance()`, `end_maintenance()`, and `SupersetDashboardsWriteMixin`.
- [ ] T060 Run quickstart.md validation: follow all steps in `specs/031-maintenance-banner/quickstart.md` (backend/frontend/Docker) and confirm no failures.
- [ ] T061 [P] Update `frontend/src/lib/i18n/` with new translation keys for maintenance banner UI strings (English + Russian placeholders): `maintenance.title`, `maintenance.active`, `maintenance.completed`, `maintenance.removeBanners`, `maintenance.removeAll`, `maintenance.settings`, `maintenance.scope`, `maintenance.excluded`, `maintenance.forced`, `maintenance.save`, `maintenance.noActiveEvents`, `maintenance.tooltip`.

---

## Dependencies & Execution Order

### Phase Dependencies

```
Phase 1: Setup ──────────────────────────────────────────────┐
Phase 2: Foundational (BLOCKS ALL STORIES) ──────────────────┤
    ├── Phase 3: US1 (P1) Banner Placement ──────────────────┤
    │   └── Phase 4: US2 (P1) Banner Removal ────────────────┤
    ├── Phase 5: US4 (P3) Settings API + Panel ──────────────┤
    │   └── Phase 6: US3 (P2) Admin UI ──────────────────────┤
    └── Phase 7: US5 (P3) Dashboard Badge ───────────────────┤
Phase 8: Polish & Cross-Cutting ─────────────────────────────┘
```

### Within Each Phase

- **Tests MUST be written and FAIL before implementation** (T016-T018 before T019+, T025-T026 before T027+, etc.)
- Models (T005-T007) → Schemas (T008) → SupersetClient (T009-T010) → SQL Extractor (T011) → Router (T012-T014)
- In US1: `find_affected_dashboards()` → `build_banner_text()` → `start_maintenance()` → route handler
- In US2: `rebuild_banner()` → `end_maintenance()` → `end_all_maintenance()` → route handlers
- In US4: Settings API backend → API client → Settings panel
- In US3: Events API backend → Store → EventsTable → Page

### Parallel Opportunities

- **Phase 1**: T002 and T003 can run in parallel
- **Phase 2**: T005, T006, T007 (models) can run in parallel; T016, T017, T018 (US1 tests) can run in parallel
- **Phase 3**: T016, T017, T018 (tests) can run in parallel
- **Phase 4**: T025, T026, T026a (tests) can run in parallel
- **Phase 6**: T038, T039 (tests) can run in parallel
- **Phase 8**: T052, T053, T054, T055, T056, T061 can run in parallel

### User Story Independence

- **US1** can be verified independently after Phase 2+3: call `/start`, check task status, verify banner in Superset
- **US2** can be verified independently after Phase 2+4: call `/end`, verify banner removal
- **US4** can be verified independently after Phase 2+5: GET/PUT /settings
- **US3** can be verified independently after Phase 2+5+6: view management page, click "Remove All"
- **US5** can be verified independently after Phase 2+7: open Dashboard Hub, see badge

Each story is an independently shippable increment. P1 stories (US1+US2) form the MVP.

---

## ADR & Rejected-Path Guardrails

| ADR / Decision | Guardrail Task | Rationale |
|----------------|---------------|-----------|
| ADR-0001 (Module Layout) | T001, T005-T007, T019-T023 | All files in canonical paths |
| ADR-0003 (Orchestrator) | T009 | Superset REST API only, no DB manipulation |
| ADR-0005 (RBAC) | T013, T023, T031, T034, T044, FR-015 | RBAC matrix enforced; dedicated `maintenance:write` permission, not `plugin:migration` |
| ADR-0006 (Svelte 5 Runes) | T041, T049 | `$state`, `$derived`, `$effect` only; no legacy `$:` or `writable` |
| ADR-0007 (fromStore REJECTED) | T041 | Store uses `$effect(() => subscribe(...))`; task T058 verifies |
| Research R1 (Direct API) | T009 | `POST /chart/` + `update_markdown_chart` not export-import; T058 verifies |
| Research R3 (Global + Jinja spans) | T011 | Jinja block spans preserved, not stripped; sqlparse only for SQL spans |
| Research R4 (TaskManager) | T023, T031 | `TaskManager.create_task()` for async execution |
| Research R5 (Dedicated Tables) | T005-T007, T005a | Not AppConfigRecord JSON; one-chart-per-dashboard via unique partial index |
| FR-015 (RBAC Matrix) | T023, T031, T034, T044 | Endpoint-by-endpoint role requirements |

---

## Notes

- Task IDs T001-T061 are sequential, plus T005a, T019a, T026a. 64 total tasks.
- Tasks marked [P] can run in parallel within their phase.
- Backend tests use `pytest -v`; frontend tests use `vitest run -v`.
- All C4 contracts (T009-T010, T021-T022, T028-T029) MUST include belief runtime instrumentation.
- Rejected-path regression is verified by T058 (ADR audit) and T059 (belief runtime audit).
- `quickstart.md` validation (T060) is the final gate before merge.
