Compare commits
2 Commits
e635bd7e5f
...
83e4875097
| Author | SHA1 | Date | |
|---|---|---|---|
| 83e4875097 | |||
| 43dd97ecbf |
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
> High-level module structure for AI Context. Generated automatically.
|
> High-level module structure for AI Context. Generated automatically.
|
||||||
|
|
||||||
**Generated:** 2026-02-23T11:15:39.876570
|
**Generated:** 2026-02-23T14:44:08.540853
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- **Total Modules:** 71
|
- **Total Modules:** 70
|
||||||
- **Total Entities:** 1340
|
- **Total Entities:** 1337
|
||||||
|
|
||||||
## Module Hierarchy
|
## Module Hierarchy
|
||||||
|
|
||||||
@@ -116,9 +116,9 @@
|
|||||||
### 📁 `__tests__/`
|
### 📁 `__tests__/`
|
||||||
|
|
||||||
- 🏗️ **Layers:** API, Domain (Tests)
|
- 🏗️ **Layers:** API, Domain (Tests)
|
||||||
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 16, TRIVIAL: 21
|
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 16, TRIVIAL: 22
|
||||||
- 📄 **Files:** 5
|
- 📄 **Files:** 5
|
||||||
- 📦 **Entities:** 40
|
- 📦 **Entities:** 41
|
||||||
|
|
||||||
**Key Entities:**
|
**Key Entities:**
|
||||||
|
|
||||||
@@ -561,9 +561,9 @@
|
|||||||
### 📁 `reports/`
|
### 📁 `reports/`
|
||||||
|
|
||||||
- 🏗️ **Layers:** Domain
|
- 🏗️ **Layers:** Domain
|
||||||
- 📊 **Tiers:** CRITICAL: 5, STANDARD: 13
|
- 📊 **Tiers:** CRITICAL: 5, STANDARD: 15
|
||||||
- 📄 **Files:** 3
|
- 📄 **Files:** 3
|
||||||
- 📦 **Entities:** 18
|
- 📦 **Entities:** 20
|
||||||
|
|
||||||
**Key Entities:**
|
**Key Entities:**
|
||||||
|
|
||||||
@@ -813,9 +813,9 @@
|
|||||||
### 📁 `layout/`
|
### 📁 `layout/`
|
||||||
|
|
||||||
- 🏗️ **Layers:** UI, Unknown
|
- 🏗️ **Layers:** UI, Unknown
|
||||||
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 4, TRIVIAL: 24
|
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 4, TRIVIAL: 26
|
||||||
- 📄 **Files:** 4
|
- 📄 **Files:** 4
|
||||||
- 📦 **Entities:** 31
|
- 📦 **Entities:** 33
|
||||||
|
|
||||||
**Key Entities:**
|
**Key Entities:**
|
||||||
|
|
||||||
@@ -851,9 +851,9 @@
|
|||||||
### 📁 `reports/`
|
### 📁 `reports/`
|
||||||
|
|
||||||
- 🏗️ **Layers:** UI, Unknown
|
- 🏗️ **Layers:** UI, Unknown
|
||||||
- 📊 **Tiers:** CRITICAL: 4, STANDARD: 1, TRIVIAL: 9
|
- 📊 **Tiers:** CRITICAL: 4, STANDARD: 1, TRIVIAL: 10
|
||||||
- 📄 **Files:** 4
|
- 📄 **Files:** 4
|
||||||
- 📦 **Entities:** 14
|
- 📦 **Entities:** 15
|
||||||
|
|
||||||
**Key Entities:**
|
**Key Entities:**
|
||||||
|
|
||||||
@@ -1235,20 +1235,6 @@
|
|||||||
- 📄 **Files:** 1
|
- 📄 **Files:** 1
|
||||||
- 📦 **Entities:** 3
|
- 📦 **Entities:** 3
|
||||||
|
|
||||||
### 📁 `tasks/`
|
|
||||||
|
|
||||||
- 🏗️ **Layers:** Page, Unknown
|
|
||||||
- 📊 **Tiers:** STANDARD: 4, TRIVIAL: 5
|
|
||||||
- 📄 **Files:** 1
|
|
||||||
- 📦 **Entities:** 9
|
|
||||||
|
|
||||||
**Key Entities:**
|
|
||||||
|
|
||||||
- 🧩 **TaskManagementPage** (Component)
|
|
||||||
- Page for managing and monitoring tasks.
|
|
||||||
- 📦 **+page** (Module) `[TRIVIAL]`
|
|
||||||
- Auto-generated module for frontend/src/routes/tasks/+page.sv...
|
|
||||||
|
|
||||||
### 📁 `debug/`
|
### 📁 `debug/`
|
||||||
|
|
||||||
- 🏗️ **Layers:** UI
|
- 🏗️ **Layers:** UI
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
- ⬅️ READS_FROM `lib`
|
- ⬅️ READS_FROM `lib`
|
||||||
- ⬅️ READS_FROM `t`
|
- ⬅️ READS_FROM `t`
|
||||||
- ƒ **spawnTask** (`Function`)
|
- ƒ **spawnTask** (`Function`)
|
||||||
|
- 📝 Execute task creation request and emit user feedback.
|
||||||
- 📦 **DashboardTypes** (`Module`) `[TRIVIAL]`
|
- 📦 **DashboardTypes** (`Module`) `[TRIVIAL]`
|
||||||
- 📝 TypeScript interfaces for Dashboard entities
|
- 📝 TypeScript interfaces for Dashboard entities
|
||||||
- 🏗️ Layer: Domain
|
- 🏗️ Layer: Domain
|
||||||
@@ -236,7 +237,9 @@
|
|||||||
- 🏗️ Layer: Domain (Tests)
|
- 🏗️ Layer: Domain (Tests)
|
||||||
- 🔒 Invariant: Sidebar store transitions must be deterministic across desktop/mobile toggles.
|
- 🔒 Invariant: Sidebar store transitions must be deterministic across desktop/mobile toggles.
|
||||||
- ƒ **test_sidebar_initial_state** (`Function`)
|
- ƒ **test_sidebar_initial_state** (`Function`)
|
||||||
|
- 📝 Verify initial sidebar store values when no persisted state is available.
|
||||||
- ƒ **test_toggleSidebar** (`Function`)
|
- ƒ **test_toggleSidebar** (`Function`)
|
||||||
|
- 📝 Verify desktop sidebar expansion toggles deterministically.
|
||||||
- ƒ **test_setActiveItem** (`Function`)
|
- ƒ **test_setActiveItem** (`Function`)
|
||||||
- ƒ **test_mobile_functions** (`Function`)
|
- ƒ **test_mobile_functions** (`Function`)
|
||||||
- 📦 **frontend.src.lib.stores.__tests__.test_activity** (`Module`)
|
- 📦 **frontend.src.lib.stores.__tests__.test_activity** (`Module`)
|
||||||
@@ -332,6 +335,8 @@
|
|||||||
- 🏗️ Layer: Unknown
|
- 🏗️ Layer: Unknown
|
||||||
- ƒ **getStatusClass** (`Function`) `[TRIVIAL]`
|
- ƒ **getStatusClass** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
|
- ƒ **getStatusLabel** (`Function`) `[TRIVIAL]`
|
||||||
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **formatDate** (`Function`) `[TRIVIAL]`
|
- ƒ **formatDate** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **onSelect** (`Function`) `[TRIVIAL]`
|
- ƒ **onSelect** (`Function`) `[TRIVIAL]`
|
||||||
@@ -412,6 +417,8 @@
|
|||||||
- 📦 **Sidebar** (`Module`) `[TRIVIAL]`
|
- 📦 **Sidebar** (`Module`) `[TRIVIAL]`
|
||||||
- 📝 Auto-generated module for frontend/src/lib/components/layout/Sidebar.svelte
|
- 📝 Auto-generated module for frontend/src/lib/components/layout/Sidebar.svelte
|
||||||
- 🏗️ Layer: Unknown
|
- 🏗️ Layer: Unknown
|
||||||
|
- ƒ **buildCategories** (`Function`) `[TRIVIAL]`
|
||||||
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **handleItemClick** (`Function`) `[TRIVIAL]`
|
- ƒ **handleItemClick** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **handleCategoryToggle** (`Function`) `[TRIVIAL]`
|
- ƒ **handleCategoryToggle** (`Function`) `[TRIVIAL]`
|
||||||
@@ -463,6 +470,8 @@
|
|||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **formatBreadcrumbLabel** (`Function`) `[TRIVIAL]`
|
- ƒ **formatBreadcrumbLabel** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
|
- ƒ **getCrumbMeta** (`Function`) `[TRIVIAL]`
|
||||||
|
- 📝 Auto-detected function (orphan)
|
||||||
- 🧩 **TaskDrawer** (`Component`) `[CRITICAL]`
|
- 🧩 **TaskDrawer** (`Component`) `[CRITICAL]`
|
||||||
- 📝 Global task drawer for monitoring background operations
|
- 📝 Global task drawer for monitoring background operations
|
||||||
- 🏗️ Layer: UI
|
- 🏗️ Layer: UI
|
||||||
@@ -481,7 +490,7 @@
|
|||||||
- 🏗️ Layer: Unknown
|
- 🏗️ Layer: Unknown
|
||||||
- ƒ **handleClose** (`Function`) `[TRIVIAL]`
|
- ƒ **handleClose** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **goToTasksPage** (`Function`) `[TRIVIAL]`
|
- ƒ **goToReportsPage** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **handleOverlayClick** (`Function`) `[TRIVIAL]`
|
- ƒ **handleOverlayClick** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
@@ -512,29 +521,6 @@
|
|||||||
- 📝 Bind global layout shell and conditional login/full-app rendering.
|
- 📝 Bind global layout shell and conditional login/full-app rendering.
|
||||||
- 🏗️ Layer: UI
|
- 🏗️ Layer: UI
|
||||||
- 🔒 Invariant: Login route bypasses shell; all other routes are wrapped by ProtectedRoute.
|
- 🔒 Invariant: Login route bypasses shell; all other routes are wrapped by ProtectedRoute.
|
||||||
- 🧩 **TaskManagementPage** (`Component`)
|
|
||||||
- 📝 Page for managing and monitoring tasks.
|
|
||||||
- 🏗️ Layer: Page
|
|
||||||
- ⬅️ READS_FROM `lib`
|
|
||||||
- ➡️ WRITES_TO `t`
|
|
||||||
- ⬅️ READS_FROM `t`
|
|
||||||
- ƒ **loadTasks** (`Function`)
|
|
||||||
- 📝 Loads tasks and environments on page initialization.
|
|
||||||
- ƒ **refreshTasks** (`Function`)
|
|
||||||
- 📝 Periodically refreshes the task list.
|
|
||||||
- ƒ **handleSelectTask** (`Function`)
|
|
||||||
- 📝 Updates the selected task ID when a task is clicked.
|
|
||||||
- 📦 **+page** (`Module`) `[TRIVIAL]`
|
|
||||||
- 📝 Auto-generated module for frontend/src/routes/tasks/+page.svelte
|
|
||||||
- 🏗️ Layer: Unknown
|
|
||||||
- ƒ **handleTaskTypeChange** (`Function`) `[TRIVIAL]`
|
|
||||||
- 📝 Auto-detected function (orphan)
|
|
||||||
- ƒ **handlePageSizeChange** (`Function`) `[TRIVIAL]`
|
|
||||||
- 📝 Auto-detected function (orphan)
|
|
||||||
- ƒ **goToPrevPage** (`Function`) `[TRIVIAL]`
|
|
||||||
- 📝 Auto-detected function (orphan)
|
|
||||||
- ƒ **goToNextPage** (`Function`) `[TRIVIAL]`
|
|
||||||
- 📝 Auto-detected function (orphan)
|
|
||||||
- 📦 **DatasetHub** (`Page`) `[CRITICAL]`
|
- 📦 **DatasetHub** (`Page`) `[CRITICAL]`
|
||||||
- 📝 Dataset Hub - Dedicated hub for datasets with mapping progress
|
- 📝 Dataset Hub - Dedicated hub for datasets with mapping progress
|
||||||
- 🏗️ Layer: UI
|
- 🏗️ Layer: UI
|
||||||
@@ -2466,6 +2452,8 @@
|
|||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **test_get_reports_filter_and_pagination** (`Function`) `[TRIVIAL]`
|
- ƒ **test_get_reports_filter_and_pagination** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
|
- ƒ **test_get_reports_handles_mixed_naive_and_aware_datetimes** (`Function`) `[TRIVIAL]`
|
||||||
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **test_get_reports_invalid_filter_returns_400** (`Function`) `[TRIVIAL]`
|
- ƒ **test_get_reports_invalid_filter_returns_400** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- 📦 **backend.src.api.routes.__tests__.test_datasets** (`Module`)
|
- 📦 **backend.src.api.routes.__tests__.test_datasets** (`Module`)
|
||||||
@@ -2473,6 +2461,7 @@
|
|||||||
- 🏗️ Layer: API
|
- 🏗️ Layer: API
|
||||||
- 🔒 Invariant: Endpoint contracts remain stable for success and validation failure paths.
|
- 🔒 Invariant: Endpoint contracts remain stable for success and validation failure paths.
|
||||||
- ƒ **test_get_datasets_success** (`Function`)
|
- ƒ **test_get_datasets_success** (`Function`)
|
||||||
|
- 📝 Validate successful datasets listing contract for an existing environment.
|
||||||
- ƒ **test_get_datasets_env_not_found** (`Function`)
|
- ƒ **test_get_datasets_env_not_found** (`Function`)
|
||||||
- ƒ **test_get_datasets_invalid_pagination** (`Function`)
|
- ƒ **test_get_datasets_invalid_pagination** (`Function`)
|
||||||
- ƒ **test_map_columns_success** (`Function`)
|
- ƒ **test_map_columns_success** (`Function`)
|
||||||
@@ -2481,6 +2470,7 @@
|
|||||||
- 📦 **backend.tests.test_reports_detail_api** (`Module`) `[CRITICAL]`
|
- 📦 **backend.tests.test_reports_detail_api** (`Module`) `[CRITICAL]`
|
||||||
- 📝 Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
- 📝 Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
||||||
- 🏗️ Layer: Domain (Tests)
|
- 🏗️ Layer: Domain (Tests)
|
||||||
|
- 🔒 Invariant: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
|
||||||
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
- ƒ **__init__** (`Function`) `[TRIVIAL]`
|
||||||
- 📝 Auto-detected function (orphan)
|
- 📝 Auto-detected function (orphan)
|
||||||
- ƒ **get_all_tasks** (`Function`) `[TRIVIAL]`
|
- ƒ **get_all_tasks** (`Function`) `[TRIVIAL]`
|
||||||
@@ -2756,6 +2746,7 @@
|
|||||||
- 🏗️ Layer: Service
|
- 🏗️ Layer: Service
|
||||||
- 🔒 Invariant: Resource summaries preserve task linkage and status projection behavior.
|
- 🔒 Invariant: Resource summaries preserve task linkage and status projection behavior.
|
||||||
- ƒ **test_get_dashboards_with_status** (`Function`)
|
- ƒ **test_get_dashboards_with_status** (`Function`)
|
||||||
|
- 📝 Validate dashboard enrichment includes git/task status projections.
|
||||||
- ƒ **test_get_datasets_with_status** (`Function`)
|
- ƒ **test_get_datasets_with_status** (`Function`)
|
||||||
- ƒ **test_get_activity_summary** (`Function`)
|
- ƒ **test_get_activity_summary** (`Function`)
|
||||||
- ƒ **test_get_git_status_for_dashboard_no_repo** (`Function`)
|
- ƒ **test_get_git_status_for_dashboard_no_repo** (`Function`)
|
||||||
@@ -2805,6 +2796,12 @@
|
|||||||
- ƒ **_load_normalized_reports** (`Function`)
|
- ƒ **_load_normalized_reports** (`Function`)
|
||||||
- 📝 Build normalized reports from all available tasks.
|
- 📝 Build normalized reports from all available tasks.
|
||||||
- 🔒 Invariant: Every returned item is a TaskReport.
|
- 🔒 Invariant: Every returned item is a TaskReport.
|
||||||
|
- ƒ **_to_utc_datetime** (`Function`)
|
||||||
|
- 📝 Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.
|
||||||
|
- 🔒 Invariant: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
|
||||||
|
- ƒ **_datetime_sort_key** (`Function`)
|
||||||
|
- 📝 Produce stable numeric sort key for report timestamps.
|
||||||
|
- 🔒 Invariant: Mixed naive/aware datetimes never raise TypeError.
|
||||||
- ƒ **_matches_query** (`Function`)
|
- ƒ **_matches_query** (`Function`)
|
||||||
- 📝 Apply query filtering to a report.
|
- 📝 Apply query filtering to a report.
|
||||||
- 🔒 Invariant: Filter evaluation is side-effect free.
|
- 🔒 Invariant: Filter evaluation is side-effect free.
|
||||||
|
|||||||
30933
.ai/openapi.json
Normal file
30933
.ai/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,8 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
|
|||||||
- SQLite (tasks.db, auth.db, migrations.db) - no new database tables required (019-superset-ux-redesign)
|
- SQLite (tasks.db, auth.db, migrations.db) - no new database tables required (019-superset-ux-redesign)
|
||||||
- Python 3.9+ (backend), Node.js 18+ (frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy/Pydantic task models, existing task/websocket stack (020-task-reports-design)
|
- Python 3.9+ (backend), Node.js 18+ (frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy/Pydantic task models, existing task/websocket stack (020-task-reports-design)
|
||||||
- SQLite task/result persistence (existing task DB), filesystem only for existing artifacts (no new primary store required) (020-task-reports-design)
|
- SQLite task/result persistence (existing task DB), filesystem only for existing artifacts (no new primary store required) (020-task-reports-design)
|
||||||
|
- Node.js 18+ runtime, SvelteKit (existing frontend stack) + SvelteKit, Tailwind CSS, existing frontend UI primitives under `frontend/src/lib/components/ui` (001-unify-frontend-style)
|
||||||
|
- N/A (UI styling and component behavior only) (001-unify-frontend-style)
|
||||||
|
|
||||||
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
||||||
|
|
||||||
@@ -63,9 +65,9 @@ cd src; pytest; ruff check .
|
|||||||
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 001-unify-frontend-style: Added Node.js 18+ runtime, SvelteKit (existing frontend stack) + SvelteKit, Tailwind CSS, existing frontend UI primitives under `frontend/src/lib/components/ui`
|
||||||
- 020-task-reports-design: Added Python 3.9+ (backend), Node.js 18+ (frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy/Pydantic task models, existing task/websocket stack
|
- 020-task-reports-design: Added Python 3.9+ (backend), Node.js 18+ (frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy/Pydantic task models, existing task/websocket stack
|
||||||
- 019-superset-ux-redesign: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy, WebSocket (existing)
|
- 019-superset-ux-redesign: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy, WebSocket (existing)
|
||||||
- 017-llm-analysis-plugin: Added Python 3.9+ (Backend), Node.js 18+ (Frontend)
|
|
||||||
|
|
||||||
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|||||||
@@ -146,6 +146,77 @@ def test_get_dashboards_invalid_pagination():
|
|||||||
# [/DEF:test_get_dashboards_invalid_pagination:Function]
|
# [/DEF:test_get_dashboards_invalid_pagination:Function]
|
||||||
|
|
||||||
|
|
||||||
|
# [DEF:test_get_dashboard_detail_success:Function]
|
||||||
|
# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets
|
||||||
|
def test_get_dashboard_detail_success():
|
||||||
|
with patch("src.api.routes.dashboards.get_config_manager") as mock_config, \
|
||||||
|
patch("src.api.routes.dashboards.has_permission") as mock_perm, \
|
||||||
|
patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
||||||
|
|
||||||
|
mock_env = MagicMock()
|
||||||
|
mock_env.id = "prod"
|
||||||
|
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||||
|
mock_perm.return_value = lambda: True
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.get_dashboard_detail.return_value = {
|
||||||
|
"id": 42,
|
||||||
|
"title": "Revenue Dashboard",
|
||||||
|
"slug": "revenue-dashboard",
|
||||||
|
"url": "/superset/dashboard/42/",
|
||||||
|
"description": "Overview",
|
||||||
|
"last_modified": "2026-02-20T10:00:00+00:00",
|
||||||
|
"published": True,
|
||||||
|
"charts": [
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"title": "Revenue by Month",
|
||||||
|
"viz_type": "line",
|
||||||
|
"dataset_id": 7,
|
||||||
|
"last_modified": "2026-02-19T10:00:00+00:00",
|
||||||
|
"overview": "line"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"table_name": "fact_revenue",
|
||||||
|
"schema": "mart",
|
||||||
|
"database": "Analytics",
|
||||||
|
"last_modified": "2026-02-18T10:00:00+00:00",
|
||||||
|
"overview": "mart.fact_revenue"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chart_count": 1,
|
||||||
|
"dataset_count": 1
|
||||||
|
}
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
|
||||||
|
response = client.get("/api/dashboards/42?env_id=prod")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["id"] == 42
|
||||||
|
assert payload["chart_count"] == 1
|
||||||
|
assert payload["dataset_count"] == 1
|
||||||
|
# [/DEF:test_get_dashboard_detail_success:Function]
|
||||||
|
|
||||||
|
|
||||||
|
# [DEF:test_get_dashboard_detail_env_not_found:Function]
|
||||||
|
# @TEST: GET /api/dashboards/{id} returns 404 for missing environment
|
||||||
|
def test_get_dashboard_detail_env_not_found():
|
||||||
|
with patch("src.api.routes.dashboards.get_config_manager") as mock_config, \
|
||||||
|
patch("src.api.routes.dashboards.has_permission") as mock_perm:
|
||||||
|
mock_config.return_value.get_environments.return_value = []
|
||||||
|
mock_perm.return_value = lambda: True
|
||||||
|
|
||||||
|
response = client.get("/api/dashboards/42?env_id=missing")
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert "Environment not found" in response.json()["detail"]
|
||||||
|
# [/DEF:test_get_dashboard_detail_env_not_found:Function]
|
||||||
|
|
||||||
|
|
||||||
# [DEF:test_migrate_dashboards_success:Function]
|
# [DEF:test_migrate_dashboards_success:Function]
|
||||||
# @TEST: POST /api/dashboards/migrate creates migration task
|
# @TEST: POST /api/dashboards/migrate creates migration task
|
||||||
# @PRE: Valid source_env_id, target_env_id, dashboard_ids
|
# @PRE: Valid source_env_id, target_env_id, dashboard_ids
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
||||||
# @LAYER: Domain (Tests)
|
# @LAYER: Domain (Tests)
|
||||||
# @RELATION: TESTS -> backend.src.api.routes.reports
|
# @RELATION: TESTS -> backend.src.api.routes.reports
|
||||||
|
# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from typing import List, Optional, Dict
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from ...dependencies import get_config_manager, get_task_manager, get_resource_service, get_mapping_service, has_permission
|
from ...dependencies import get_config_manager, get_task_manager, get_resource_service, get_mapping_service, has_permission
|
||||||
from ...core.logger import logger, belief_scope
|
from ...core.logger import logger, belief_scope
|
||||||
|
from ...core.superset_client import SupersetClient
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"])
|
router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"])
|
||||||
@@ -52,6 +53,41 @@ class DashboardsResponse(BaseModel):
|
|||||||
total_pages: int
|
total_pages: int
|
||||||
# [/DEF:DashboardsResponse:DataClass]
|
# [/DEF:DashboardsResponse:DataClass]
|
||||||
|
|
||||||
|
# [DEF:DashboardChartItem:DataClass]
|
||||||
|
class DashboardChartItem(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
viz_type: Optional[str] = None
|
||||||
|
dataset_id: Optional[int] = None
|
||||||
|
last_modified: Optional[str] = None
|
||||||
|
overview: Optional[str] = None
|
||||||
|
# [/DEF:DashboardChartItem:DataClass]
|
||||||
|
|
||||||
|
# [DEF:DashboardDatasetItem:DataClass]
|
||||||
|
class DashboardDatasetItem(BaseModel):
|
||||||
|
id: int
|
||||||
|
table_name: str
|
||||||
|
schema: Optional[str] = None
|
||||||
|
database: str
|
||||||
|
last_modified: Optional[str] = None
|
||||||
|
overview: Optional[str] = None
|
||||||
|
# [/DEF:DashboardDatasetItem:DataClass]
|
||||||
|
|
||||||
|
# [DEF:DashboardDetailResponse:DataClass]
|
||||||
|
class DashboardDetailResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
slug: Optional[str] = None
|
||||||
|
url: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
last_modified: Optional[str] = None
|
||||||
|
published: Optional[bool] = None
|
||||||
|
charts: List[DashboardChartItem]
|
||||||
|
datasets: List[DashboardDatasetItem]
|
||||||
|
chart_count: int
|
||||||
|
dataset_count: int
|
||||||
|
# [/DEF:DashboardDetailResponse:DataClass]
|
||||||
|
|
||||||
# [DEF:get_dashboards:Function]
|
# [DEF:get_dashboards:Function]
|
||||||
# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status
|
# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status
|
||||||
# @PRE: env_id must be a valid environment ID
|
# @PRE: env_id must be a valid environment ID
|
||||||
@@ -132,6 +168,39 @@ async def get_dashboards(
|
|||||||
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboards: {str(e)}")
|
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboards: {str(e)}")
|
||||||
# [/DEF:get_dashboards:Function]
|
# [/DEF:get_dashboards:Function]
|
||||||
|
|
||||||
|
# [DEF:get_dashboard_detail:Function]
|
||||||
|
# @PURPOSE: Fetch detailed dashboard info with related charts and datasets
|
||||||
|
# @PRE: env_id must be valid and dashboard_id must exist
|
||||||
|
# @POST: Returns dashboard detail payload for overview page
|
||||||
|
# @RELATION: CALLS -> SupersetClient.get_dashboard_detail
|
||||||
|
@router.get("/{dashboard_id}", response_model=DashboardDetailResponse)
|
||||||
|
async def get_dashboard_detail(
|
||||||
|
dashboard_id: int,
|
||||||
|
env_id: str,
|
||||||
|
config_manager=Depends(get_config_manager),
|
||||||
|
_ = Depends(has_permission("plugin:migration", "READ"))
|
||||||
|
):
|
||||||
|
with belief_scope("get_dashboard_detail", f"dashboard_id={dashboard_id}, env_id={env_id}"):
|
||||||
|
environments = config_manager.get_environments()
|
||||||
|
env = next((e for e in environments if e.id == env_id), None)
|
||||||
|
if not env:
|
||||||
|
logger.error(f"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}")
|
||||||
|
raise HTTPException(status_code=404, detail="Environment not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = SupersetClient(env)
|
||||||
|
detail = client.get_dashboard_detail(dashboard_id)
|
||||||
|
logger.info(
|
||||||
|
f"[get_dashboard_detail][Coherence:OK] Dashboard {dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets"
|
||||||
|
)
|
||||||
|
return DashboardDetailResponse(**detail)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}")
|
||||||
|
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboard detail: {str(e)}")
|
||||||
|
# [/DEF:get_dashboard_detail:Function]
|
||||||
|
|
||||||
# [DEF:MigrateRequest:DataClass]
|
# [DEF:MigrateRequest:DataClass]
|
||||||
class MigrateRequest(BaseModel):
|
class MigrateRequest(BaseModel):
|
||||||
source_env_id: str = Field(..., description="Source environment ID")
|
source_env_id: str = Field(..., description="Source environment ID")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
# [SECTION: IMPORTS]
|
# [SECTION: IMPORTS]
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple, Union, cast
|
from typing import Dict, List, Optional, Tuple, Union, cast
|
||||||
@@ -120,6 +121,252 @@ class SupersetClient:
|
|||||||
return result
|
return result
|
||||||
# [/DEF:get_dashboards_summary:Function]
|
# [/DEF:get_dashboards_summary:Function]
|
||||||
|
|
||||||
|
# [DEF:get_dashboard:Function]
|
||||||
|
# @PURPOSE: Fetches a single dashboard by ID.
|
||||||
|
# @PRE: Client is authenticated and dashboard_id exists.
|
||||||
|
# @POST: Returns dashboard payload from Superset API.
|
||||||
|
# @RETURN: Dict
|
||||||
|
def get_dashboard(self, dashboard_id: int) -> Dict:
|
||||||
|
with belief_scope("SupersetClient.get_dashboard", f"id={dashboard_id}"):
|
||||||
|
response = self.network.request(method="GET", endpoint=f"/dashboard/{dashboard_id}")
|
||||||
|
return cast(Dict, response)
|
||||||
|
# [/DEF:get_dashboard:Function]
|
||||||
|
|
||||||
|
# [DEF:get_chart:Function]
|
||||||
|
# @PURPOSE: Fetches a single chart by ID.
|
||||||
|
# @PRE: Client is authenticated and chart_id exists.
|
||||||
|
# @POST: Returns chart payload from Superset API.
|
||||||
|
# @RETURN: Dict
|
||||||
|
def get_chart(self, chart_id: int) -> Dict:
|
||||||
|
with belief_scope("SupersetClient.get_chart", f"id={chart_id}"):
|
||||||
|
response = self.network.request(method="GET", endpoint=f"/chart/{chart_id}")
|
||||||
|
return cast(Dict, response)
|
||||||
|
# [/DEF:get_chart:Function]
|
||||||
|
|
||||||
|
# [DEF:get_dashboard_detail:Function]
|
||||||
|
# @PURPOSE: Fetches detailed dashboard information including related charts and datasets.
|
||||||
|
# @PRE: Client is authenticated and dashboard_id exists.
|
||||||
|
# @POST: Returns dashboard metadata with charts and datasets lists.
|
||||||
|
# @RETURN: Dict
|
||||||
|
def get_dashboard_detail(self, dashboard_id: int) -> Dict:
|
||||||
|
with belief_scope("SupersetClient.get_dashboard_detail", f"id={dashboard_id}"):
|
||||||
|
dashboard_response = self.get_dashboard(dashboard_id)
|
||||||
|
dashboard_data = dashboard_response.get("result", dashboard_response)
|
||||||
|
|
||||||
|
charts: List[Dict] = []
|
||||||
|
datasets: List[Dict] = []
|
||||||
|
|
||||||
|
def extract_dataset_id_from_form_data(form_data: Optional[Dict]) -> Optional[int]:
|
||||||
|
if not isinstance(form_data, dict):
|
||||||
|
return None
|
||||||
|
datasource = form_data.get("datasource")
|
||||||
|
if isinstance(datasource, str):
|
||||||
|
matched = re.match(r"^(\d+)__", datasource)
|
||||||
|
if matched:
|
||||||
|
try:
|
||||||
|
return int(matched.group(1))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if isinstance(datasource, dict):
|
||||||
|
ds_id = datasource.get("id")
|
||||||
|
try:
|
||||||
|
return int(ds_id) if ds_id is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
ds_id = form_data.get("datasource_id")
|
||||||
|
try:
|
||||||
|
return int(ds_id) if ds_id is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Canonical endpoints from Superset OpenAPI:
|
||||||
|
# /dashboard/{id_or_slug}/charts and /dashboard/{id_or_slug}/datasets.
|
||||||
|
try:
|
||||||
|
charts_response = self.network.request(
|
||||||
|
method="GET",
|
||||||
|
endpoint=f"/dashboard/{dashboard_id}/charts"
|
||||||
|
)
|
||||||
|
charts_payload = charts_response.get("result", []) if isinstance(charts_response, dict) else []
|
||||||
|
for chart_obj in charts_payload:
|
||||||
|
if not isinstance(chart_obj, dict):
|
||||||
|
continue
|
||||||
|
chart_id = chart_obj.get("id")
|
||||||
|
if chart_id is None:
|
||||||
|
continue
|
||||||
|
form_data = chart_obj.get("form_data")
|
||||||
|
if isinstance(form_data, str):
|
||||||
|
try:
|
||||||
|
form_data = json.loads(form_data)
|
||||||
|
except Exception:
|
||||||
|
form_data = {}
|
||||||
|
dataset_id = extract_dataset_id_from_form_data(form_data) or chart_obj.get("datasource_id")
|
||||||
|
charts.append({
|
||||||
|
"id": int(chart_id),
|
||||||
|
"title": chart_obj.get("slice_name") or chart_obj.get("name") or f"Chart {chart_id}",
|
||||||
|
"viz_type": (form_data.get("viz_type") if isinstance(form_data, dict) else None),
|
||||||
|
"dataset_id": int(dataset_id) if dataset_id is not None else None,
|
||||||
|
"last_modified": chart_obj.get("changed_on"),
|
||||||
|
"overview": chart_obj.get("description") or (form_data.get("viz_type") if isinstance(form_data, dict) else None) or "Chart",
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.warning("[get_dashboard_detail][Warning] Failed to fetch dashboard charts: %s", e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
datasets_response = self.network.request(
|
||||||
|
method="GET",
|
||||||
|
endpoint=f"/dashboard/{dashboard_id}/datasets"
|
||||||
|
)
|
||||||
|
datasets_payload = datasets_response.get("result", []) if isinstance(datasets_response, dict) else []
|
||||||
|
for dataset_obj in datasets_payload:
|
||||||
|
if not isinstance(dataset_obj, dict):
|
||||||
|
continue
|
||||||
|
dataset_id = dataset_obj.get("id")
|
||||||
|
if dataset_id is None:
|
||||||
|
continue
|
||||||
|
db_payload = dataset_obj.get("database")
|
||||||
|
db_name = db_payload.get("database_name") if isinstance(db_payload, dict) else None
|
||||||
|
table_name = dataset_obj.get("table_name") or dataset_obj.get("datasource_name") or dataset_obj.get("name") or f"Dataset {dataset_id}"
|
||||||
|
schema = dataset_obj.get("schema")
|
||||||
|
fq_name = f"{schema}.{table_name}" if schema else table_name
|
||||||
|
datasets.append({
|
||||||
|
"id": int(dataset_id),
|
||||||
|
"table_name": table_name,
|
||||||
|
"schema": schema,
|
||||||
|
"database": db_name or dataset_obj.get("database_name") or "Unknown",
|
||||||
|
"last_modified": dataset_obj.get("changed_on"),
|
||||||
|
"overview": fq_name,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.warning("[get_dashboard_detail][Warning] Failed to fetch dashboard datasets: %s", e)
|
||||||
|
|
||||||
|
# Fallback: derive chart IDs from layout metadata if dashboard charts endpoint fails.
|
||||||
|
if not charts:
|
||||||
|
raw_position_json = dashboard_data.get("position_json")
|
||||||
|
chart_ids_from_position = set()
|
||||||
|
if isinstance(raw_position_json, str) and raw_position_json:
|
||||||
|
try:
|
||||||
|
parsed_position = json.loads(raw_position_json)
|
||||||
|
chart_ids_from_position.update(self._extract_chart_ids_from_layout(parsed_position))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif isinstance(raw_position_json, dict):
|
||||||
|
chart_ids_from_position.update(self._extract_chart_ids_from_layout(raw_position_json))
|
||||||
|
|
||||||
|
raw_json_metadata = dashboard_data.get("json_metadata")
|
||||||
|
if isinstance(raw_json_metadata, str) and raw_json_metadata:
|
||||||
|
try:
|
||||||
|
parsed_metadata = json.loads(raw_json_metadata)
|
||||||
|
chart_ids_from_position.update(self._extract_chart_ids_from_layout(parsed_metadata))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif isinstance(raw_json_metadata, dict):
|
||||||
|
chart_ids_from_position.update(self._extract_chart_ids_from_layout(raw_json_metadata))
|
||||||
|
|
||||||
|
app_logger.info(
|
||||||
|
"[get_dashboard_detail][State] Extracted %s fallback chart IDs from layout (dashboard_id=%s)",
|
||||||
|
len(chart_ids_from_position),
|
||||||
|
dashboard_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for chart_id in sorted(chart_ids_from_position):
|
||||||
|
try:
|
||||||
|
chart_response = self.get_chart(int(chart_id))
|
||||||
|
chart_data = chart_response.get("result", chart_response)
|
||||||
|
charts.append({
|
||||||
|
"id": int(chart_id),
|
||||||
|
"title": chart_data.get("slice_name") or chart_data.get("name") or f"Chart {chart_id}",
|
||||||
|
"viz_type": chart_data.get("viz_type"),
|
||||||
|
"dataset_id": chart_data.get("datasource_id"),
|
||||||
|
"last_modified": chart_data.get("changed_on"),
|
||||||
|
"overview": chart_data.get("description") or chart_data.get("viz_type") or "Chart",
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.warning("[get_dashboard_detail][Warning] Failed to resolve fallback chart %s: %s", chart_id, e)
|
||||||
|
|
||||||
|
# Backfill datasets from chart datasource IDs.
|
||||||
|
dataset_ids_from_charts = {
|
||||||
|
c.get("dataset_id")
|
||||||
|
for c in charts
|
||||||
|
if c.get("dataset_id") is not None
|
||||||
|
}
|
||||||
|
known_dataset_ids = {d.get("id") for d in datasets}
|
||||||
|
missing_dataset_ids = [ds_id for ds_id in dataset_ids_from_charts if ds_id not in known_dataset_ids]
|
||||||
|
|
||||||
|
for dataset_id in missing_dataset_ids:
|
||||||
|
try:
|
||||||
|
dataset_response = self.get_dataset(int(dataset_id))
|
||||||
|
dataset_data = dataset_response.get("result", dataset_response)
|
||||||
|
db_payload = dataset_data.get("database")
|
||||||
|
db_name = db_payload.get("database_name") if isinstance(db_payload, dict) else None
|
||||||
|
table_name = dataset_data.get("table_name") or f"Dataset {dataset_id}"
|
||||||
|
schema = dataset_data.get("schema")
|
||||||
|
fq_name = f"{schema}.{table_name}" if schema else table_name
|
||||||
|
datasets.append({
|
||||||
|
"id": int(dataset_id),
|
||||||
|
"table_name": table_name,
|
||||||
|
"schema": schema,
|
||||||
|
"database": db_name or "Unknown",
|
||||||
|
"last_modified": dataset_data.get("changed_on_utc") or dataset_data.get("changed_on"),
|
||||||
|
"overview": fq_name,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.warning("[get_dashboard_detail][Warning] Failed to resolve dataset %s: %s", dataset_id, e)
|
||||||
|
|
||||||
|
unique_charts = {}
|
||||||
|
for chart in charts:
|
||||||
|
unique_charts[chart["id"]] = chart
|
||||||
|
|
||||||
|
unique_datasets = {}
|
||||||
|
for dataset in datasets:
|
||||||
|
unique_datasets[dataset["id"]] = dataset
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": dashboard_data.get("id", dashboard_id),
|
||||||
|
"title": dashboard_data.get("dashboard_title") or dashboard_data.get("title") or f"Dashboard {dashboard_id}",
|
||||||
|
"slug": dashboard_data.get("slug"),
|
||||||
|
"url": dashboard_data.get("url"),
|
||||||
|
"description": dashboard_data.get("description") or "",
|
||||||
|
"last_modified": dashboard_data.get("changed_on_utc") or dashboard_data.get("changed_on"),
|
||||||
|
"published": dashboard_data.get("published"),
|
||||||
|
"charts": list(unique_charts.values()),
|
||||||
|
"datasets": list(unique_datasets.values()),
|
||||||
|
"chart_count": len(unique_charts),
|
||||||
|
"dataset_count": len(unique_datasets),
|
||||||
|
}
|
||||||
|
# [/DEF:get_dashboard_detail:Function]
|
||||||
|
|
||||||
|
# [DEF:_extract_chart_ids_from_layout:Function]
|
||||||
|
# @PURPOSE: Traverses dashboard layout metadata and extracts chart IDs from common keys.
|
||||||
|
# @PRE: payload can be dict/list/scalar.
|
||||||
|
# @POST: Returns a set of chart IDs found in nested structures.
|
||||||
|
def _extract_chart_ids_from_layout(self, payload: Union[Dict, List, str, int, None]) -> set:
|
||||||
|
with belief_scope("_extract_chart_ids_from_layout"):
|
||||||
|
found = set()
|
||||||
|
|
||||||
|
def walk(node):
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for key, value in node.items():
|
||||||
|
if key in ("chartId", "chart_id", "slice_id", "sliceId"):
|
||||||
|
try:
|
||||||
|
found.add(int(value))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if key == "id" and isinstance(value, str):
|
||||||
|
match = re.match(r"^CHART-(\d+)$", value)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
found.add(int(match.group(1)))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
walk(value)
|
||||||
|
elif isinstance(node, list):
|
||||||
|
for item in node:
|
||||||
|
walk(item)
|
||||||
|
|
||||||
|
walk(payload)
|
||||||
|
return found
|
||||||
|
# [/DEF:_extract_chart_ids_from_layout:Function]
|
||||||
|
|
||||||
# [DEF:export_dashboard:Function]
|
# [DEF:export_dashboard:Function]
|
||||||
# @PURPOSE: Экспортирует дашборд в виде ZIP-архива.
|
# @PURPOSE: Экспортирует дашборд в виде ZIP-архива.
|
||||||
# @PARAM: dashboard_id (int) - ID дашборда для экспорта.
|
# @PARAM: dashboard_id (int) - ID дашборда для экспорта.
|
||||||
@@ -246,6 +493,15 @@ class SupersetClient:
|
|||||||
# @RELATION: CALLS -> self.network.request (for related_objects)
|
# @RELATION: CALLS -> self.network.request (for related_objects)
|
||||||
def get_dataset_detail(self, dataset_id: int) -> Dict:
|
def get_dataset_detail(self, dataset_id: int) -> Dict:
|
||||||
with belief_scope("SupersetClient.get_dataset_detail", f"id={dataset_id}"):
|
with belief_scope("SupersetClient.get_dataset_detail", f"id={dataset_id}"):
|
||||||
|
def as_bool(value, default=False):
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip().lower() in ("1", "true", "yes", "y", "on")
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
# Get base dataset info
|
# Get base dataset info
|
||||||
response = self.get_dataset(dataset_id)
|
response = self.get_dataset(dataset_id)
|
||||||
|
|
||||||
@@ -259,12 +515,15 @@ class SupersetClient:
|
|||||||
columns = dataset.get("columns", [])
|
columns = dataset.get("columns", [])
|
||||||
column_info = []
|
column_info = []
|
||||||
for col in columns:
|
for col in columns:
|
||||||
|
col_id = col.get("id")
|
||||||
|
if col_id is None:
|
||||||
|
continue
|
||||||
column_info.append({
|
column_info.append({
|
||||||
"id": col.get("id"),
|
"id": int(col_id),
|
||||||
"name": col.get("column_name"),
|
"name": col.get("column_name"),
|
||||||
"type": col.get("type"),
|
"type": col.get("type"),
|
||||||
"is_dttm": col.get("is_dttm", False),
|
"is_dttm": as_bool(col.get("is_dttm"), default=False),
|
||||||
"is_active": col.get("is_active", True),
|
"is_active": as_bool(col.get("is_active"), default=True),
|
||||||
"description": col.get("description", "")
|
"description": col.get("description", "")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -286,11 +545,25 @@ class SupersetClient:
|
|||||||
dashboards_data = []
|
dashboards_data = []
|
||||||
|
|
||||||
for dash in dashboards_data:
|
for dash in dashboards_data:
|
||||||
|
if isinstance(dash, dict):
|
||||||
|
dash_id = dash.get("id")
|
||||||
|
if dash_id is None:
|
||||||
|
continue
|
||||||
linked_dashboards.append({
|
linked_dashboards.append({
|
||||||
"id": dash.get("id"),
|
"id": int(dash_id),
|
||||||
"title": dash.get("dashboard_title") or dash.get("title", "Unknown"),
|
"title": dash.get("dashboard_title") or dash.get("title", f"Dashboard {dash_id}"),
|
||||||
"slug": dash.get("slug")
|
"slug": dash.get("slug")
|
||||||
})
|
})
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
dash_id = int(dash)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
linked_dashboards.append({
|
||||||
|
"id": dash_id,
|
||||||
|
"title": f"Dashboard {dash_id}",
|
||||||
|
"slug": None
|
||||||
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_logger.warning(f"[get_dataset_detail][Warning] Failed to fetch related dashboards: {e}")
|
app_logger.warning(f"[get_dataset_detail][Warning] Failed to fetch related dashboards: {e}")
|
||||||
linked_dashboards = []
|
linked_dashboards = []
|
||||||
@@ -302,14 +575,18 @@ class SupersetClient:
|
|||||||
"id": dataset.get("id"),
|
"id": dataset.get("id"),
|
||||||
"table_name": dataset.get("table_name"),
|
"table_name": dataset.get("table_name"),
|
||||||
"schema": dataset.get("schema"),
|
"schema": dataset.get("schema"),
|
||||||
"database": dataset.get("database", {}).get("database_name", "Unknown"),
|
"database": (
|
||||||
|
dataset.get("database", {}).get("database_name", "Unknown")
|
||||||
|
if isinstance(dataset.get("database"), dict)
|
||||||
|
else dataset.get("database_name") or "Unknown"
|
||||||
|
),
|
||||||
"description": dataset.get("description", ""),
|
"description": dataset.get("description", ""),
|
||||||
"columns": column_info,
|
"columns": column_info,
|
||||||
"column_count": len(column_info),
|
"column_count": len(column_info),
|
||||||
"sql": sql,
|
"sql": sql,
|
||||||
"linked_dashboards": linked_dashboards,
|
"linked_dashboards": linked_dashboards,
|
||||||
"linked_dashboard_count": len(linked_dashboards),
|
"linked_dashboard_count": len(linked_dashboards),
|
||||||
"is_sqllab_view": dataset.get("is_sqllab_view", False),
|
"is_sqllab_view": as_bool(dataset.get("is_sqllab_view"), default=False),
|
||||||
"created_on": dataset.get("created_on"),
|
"created_on": dataset.get("created_on"),
|
||||||
"changed_on": dataset.get("changed_on")
|
"changed_on": dataset.get("changed_on")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
app:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: docker/backend.Dockerfile
|
||||||
container_name: ss_tools_app
|
container_name: ss_tools_backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -33,11 +33,22 @@ services:
|
|||||||
AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
|
AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
|
||||||
BACKEND_PORT: 8000
|
BACKEND_PORT: 8000
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "${BACKEND_HOST_PORT:-8001}:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.json:/app/config.json
|
- ./config.json:/app/config.json
|
||||||
- ./backups:/app/backups
|
- ./backups:/app/backups
|
||||||
- ./backend/git_repos:/app/backend/git_repos
|
- ./backend/git_repos:/app/backend/git_repos
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/frontend.Dockerfile
|
||||||
|
container_name: ss_tools_frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_HOST_PORT:-8000}:80"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
# Stage 1: Build frontend static assets
|
FROM python:3.11-slim
|
||||||
FROM node:20-alpine AS frontend-build
|
|
||||||
WORKDIR /app/frontend
|
|
||||||
|
|
||||||
COPY frontend/package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY frontend/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
|
|
||||||
# Stage 2: Runtime image for backend + static frontend
|
|
||||||
FROM python:3.11-slim AS runtime
|
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
@@ -28,7 +16,6 @@ RUN pip install --no-cache-dir -r /app/backend/requirements.txt
|
|||||||
RUN python -m playwright install --with-deps chromium
|
RUN python -m playwright install --with-deps chromium
|
||||||
|
|
||||||
COPY backend/ /app/backend/
|
COPY backend/ /app/backend/
|
||||||
COPY --from=frontend-build /app/frontend/build /app/frontend/build
|
|
||||||
|
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
|
|
||||||
16
docker/frontend.Dockerfile
Normal file
16
docker/frontend.Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/frontend/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
31
docker/nginx.conf
Normal file
31
docker/nginx.conf
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:8000/api/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://backend:8000/ws/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,6 +187,7 @@ export const api = {
|
|||||||
if (options.page_size) params.append('page_size', options.page_size);
|
if (options.page_size) params.append('page_size', options.page_size);
|
||||||
return fetchApi(`/dashboards?${params.toString()}`);
|
return fetchApi(`/dashboards?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
getDashboardDetail: (envId, dashboardId) => fetchApi(`/dashboards/${dashboardId}?env_id=${envId}`),
|
||||||
getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||||
|
|
||||||
// Datasets
|
// Datasets
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div
|
<div
|
||||||
class="bg-white border-r border-gray-200 flex flex-col h-screen fixed left-0 top-0 z-30 transition-[width] duration-200 ease-in-out
|
class="fixed left-0 top-0 z-30 flex h-screen flex-col border-r border-slate-200 bg-white shadow-sm transition-[width] duration-200 ease-in-out
|
||||||
{isExpanded ? 'w-sidebar' : 'w-sidebar-collapsed'}
|
{isExpanded ? 'w-sidebar' : 'w-sidebar-collapsed'}
|
||||||
{isMobileOpen
|
{isMobileOpen
|
||||||
? 'translate-x-0 w-sidebar'
|
? 'translate-x-0 w-sidebar'
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center p-4 border-b border-gray-200 {isExpanded
|
class="flex items-center border-b border-slate-200 p-4 {isExpanded
|
||||||
? 'justify-between'
|
? 'justify-between'
|
||||||
: 'justify-center'}"
|
: 'justify-center'}"
|
||||||
>
|
>
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<!-- Category Header -->
|
<!-- Category Header -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between px-4 py-3 cursor-pointer transition-colors hover:bg-gray-100
|
class="flex cursor-pointer items-center justify-between px-4 py-3 transition-colors hover:bg-slate-100
|
||||||
{activeCategory === category.id
|
{activeCategory === category.id
|
||||||
? 'bg-primary-light text-primary md:border-r-2 md:border-primary'
|
? 'bg-primary-light text-primary md:border-r-2 md:border-primary'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
<!-- Drawer Overlay -->
|
<!-- Drawer Overlay -->
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black/40 backdrop-blur-sm z-50"
|
class="fixed inset-0 z-50 bg-black/35 backdrop-blur-sm"
|
||||||
on:click={handleOverlayClick}
|
on:click={handleOverlayClick}
|
||||||
on:keydown={(e) => e.key === 'Escape' && handleClose()}
|
on:keydown={(e) => e.key === 'Escape' && handleClose()}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -200,13 +200,13 @@
|
|||||||
>
|
>
|
||||||
<!-- Drawer Panel -->
|
<!-- Drawer Panel -->
|
||||||
<div
|
<div
|
||||||
class="fixed right-0 top-0 h-full w-full max-w-[560px] bg-slate-900 shadow-[-8px_0_30px_rgba(0,0,0,0.3)] flex flex-col z-50 transition-transform duration-300 ease-out"
|
class="fixed right-0 top-0 z-50 flex h-full w-full max-w-[560px] flex-col border-l border-slate-200 bg-white shadow-[-8px_0_30px_rgba(15,23,42,0.15)] transition-transform duration-300 ease-out"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Task drawer"
|
aria-label="Task drawer"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between px-5 py-3.5 border-b border-slate-800 bg-slate-900">
|
<div class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
{#if !activeTaskId && recentTasks.length > 0}
|
{#if !activeTaskId && recentTasks.length > 0}
|
||||||
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400">
|
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400">
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
<Icon name="back" size={16} strokeWidth={2} />
|
<Icon name="back" size={16} strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<h2 class="text-sm font-semibold text-slate-100 tracking-tight">
|
<h2 class="text-sm font-semibold tracking-tight text-slate-900">
|
||||||
{activeTaskId ? ($t.tasks?.details_logs || 'Task Details & Logs') : 'Recent Tasks'}
|
{activeTaskId ? ($t.tasks?.details_logs || 'Task Details & Logs') : 'Recent Tasks'}
|
||||||
</h2>
|
</h2>
|
||||||
{#if shortTaskId}
|
{#if shortTaskId}
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-1 text-xs font-semibold rounded-md border border-slate-700 text-slate-300 bg-slate-800/60 hover:bg-slate-800 transition-colors"
|
class="rounded-md border border-slate-300 bg-slate-50 px-2.5 py-1 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100"
|
||||||
on:click={goToReportsPage}
|
on:click={goToReportsPage}
|
||||||
>
|
>
|
||||||
{$t.nav?.reports || "Reports"}
|
{$t.nav?.reports || "Reports"}
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
/>
|
/>
|
||||||
{:else if loadingTasks}
|
{:else if loadingTasks}
|
||||||
<div class="flex flex-col items-center justify-center p-12 text-slate-500">
|
<div class="flex flex-col items-center justify-center p-12 text-slate-500">
|
||||||
<div class="w-8 h-8 border-2 border-slate-200 border-t-blue-500 rounded-full animate-spin mb-4"></div>
|
<div class="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500"></div>
|
||||||
<p>Loading tasks...</p>
|
<p>Loading tasks...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if recentTasks.length > 0}
|
{:else if recentTasks.length > 0}
|
||||||
|
|||||||
@@ -89,14 +89,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="bg-white border-b border-gray-200 fixed top-0 right-0 left-0 h-16 flex items-center justify-between px-4 z-40
|
class="fixed left-0 right-0 top-0 z-40 flex h-16 items-center justify-between border-b border-slate-200 bg-white px-4 shadow-sm
|
||||||
{isExpanded ? 'md:left-[240px]' : 'md:left-16'}"
|
{isExpanded ? 'md:left-[240px]' : 'md:left-16'}"
|
||||||
>
|
>
|
||||||
<!-- Left section: Hamburger (mobile) + Logo -->
|
<!-- Left section: Hamburger (mobile) + Logo -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Hamburger Menu (mobile only) -->
|
<!-- Hamburger Menu (mobile only) -->
|
||||||
<button
|
<button
|
||||||
class="p-2 rounded-lg hover:bg-gray-100 text-gray-600 md:hidden"
|
class="rounded-lg p-2 text-slate-600 transition-colors hover:bg-slate-100 md:hidden"
|
||||||
on:click={handleHamburgerClick}
|
on:click={handleHamburgerClick}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<!-- Logo/Brand -->
|
<!-- Logo/Brand -->
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="flex items-center text-xl font-bold text-gray-800 hover:text-primary transition-colors"
|
class="flex items-center text-xl font-bold text-slate-800 transition-colors hover:text-primary"
|
||||||
>
|
>
|
||||||
<span class="mr-2 inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 via-cyan-500 to-indigo-600 text-white shadow-sm">
|
<span class="mr-2 inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 via-cyan-500 to-indigo-600 text-white shadow-sm">
|
||||||
<Icon name="layers" size={18} strokeWidth={2.1} />
|
<Icon name="layers" size={18} strokeWidth={2.1} />
|
||||||
@@ -128,10 +128,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nav Actions -->
|
<!-- Nav Actions -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center gap-3 md:gap-4">
|
||||||
<!-- Activity Indicator -->
|
<!-- Activity Indicator -->
|
||||||
<div
|
<div
|
||||||
class="relative cursor-pointer p-2 rounded-lg hover:bg-gray-100 transition-colors text-slate-600"
|
class="relative cursor-pointer rounded-lg p-2 text-slate-600 transition-colors hover:bg-slate-100"
|
||||||
on:click={handleActivityClick}
|
on:click={handleActivityClick}
|
||||||
on:keydown={(e) =>
|
on:keydown={(e) =>
|
||||||
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
(e.key === "Enter" || e.key === " ") && handleActivityClick()}
|
||||||
|
|||||||
@@ -24,11 +24,19 @@
|
|||||||
const profileLabel = $derived(typeof profile?.label === 'function' ? profile.label() : profile?.label);
|
const profileLabel = $derived(typeof profile?.label === 'function' ? profile.label() : profile?.label);
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (status === 'success') return 'bg-green-100 text-green-700';
|
if (status === 'success') return 'bg-green-100 text-green-700 ring-1 ring-green-200';
|
||||||
if (status === 'failed') return 'bg-red-100 text-red-700';
|
if (status === 'failed') return 'bg-red-100 text-red-700 ring-1 ring-red-200';
|
||||||
if (status === 'in_progress') return 'bg-blue-100 text-blue-700';
|
if (status === 'in_progress') return 'bg-blue-100 text-blue-700 ring-1 ring-blue-200';
|
||||||
if (status === 'partial') return 'bg-amber-100 text-amber-700';
|
if (status === 'partial') return 'bg-amber-100 text-amber-700 ring-1 ring-amber-200';
|
||||||
return 'bg-slate-100 text-slate-700';
|
return 'bg-slate-100 text-slate-700 ring-1 ring-slate-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusLabel(status) {
|
||||||
|
if (status === 'success') return $t.reports?.status_success || 'Success';
|
||||||
|
if (status === 'failed') return $t.reports?.status_failed || 'Failed';
|
||||||
|
if (status === 'in_progress') return $t.reports?.status_in_progress || 'In progress';
|
||||||
|
if (status === 'partial') return $t.reports?.status_partial || 'Partial';
|
||||||
|
return status || ($t.reports?.not_provided || 'Not provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
@@ -44,7 +52,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full rounded-lg border p-3 text-left transition hover:bg-slate-50 {selected ? 'border-blue-400 bg-blue-50' : 'border-slate-200 bg-white'}"
|
class="w-full rounded-xl border p-4 text-left shadow-sm transition hover:border-slate-300 hover:bg-slate-50 hover:shadow {selected ? 'border-blue-400 bg-blue-50' : 'border-slate-200 bg-white'}"
|
||||||
on:click={onSelect}
|
on:click={onSelect}
|
||||||
aria-label={`Report ${report?.report_id || ''} type ${profileLabel || ($t.reports?.unknown_type || 'Other / Unknown Type')}`}
|
aria-label={`Report ${report?.report_id || ''} type ${profileLabel || ($t.reports?.unknown_type || 'Other / Unknown Type')}`}
|
||||||
>
|
>
|
||||||
@@ -53,7 +61,7 @@
|
|||||||
{profileLabel || ($t.reports?.unknown_type || 'Other / Unknown Type')}
|
{profileLabel || ($t.reports?.unknown_type || 'Other / Unknown Type')}
|
||||||
</span>
|
</span>
|
||||||
<span class="rounded px-2 py-0.5 text-xs font-semibold {getStatusClass(report?.status)}">
|
<span class="rounded px-2 py-0.5 text-xs font-semibold {getStatusClass(report?.status)}">
|
||||||
{report?.status || ($t.reports?.not_provided || 'Not provided')}
|
{getStatusLabel(report?.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm font-medium text-slate-800">{report?.summary || ($t.reports?.not_provided || 'Not provided')}</p>
|
<p class="text-sm font-medium text-slate-800">{report?.summary || ($t.reports?.not_provided || 'Not provided')}</p>
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||||
<h3 class="mb-3 text-sm font-semibold text-slate-700">{$t.reports?.view_details || 'View details'}</h3>
|
<h3 class="mb-3 text-sm font-semibold text-slate-700">{$t.reports?.view_details || 'View details'}</h3>
|
||||||
|
|
||||||
{#if !detail || !detail.report}
|
{#if !detail || !detail.report}
|
||||||
<p class="text-sm text-slate-500">{$t.reports?.not_provided || 'Not provided'}</p>
|
<p class="text-sm text-slate-500">{$t.reports?.not_provided || 'Not provided'}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm text-slate-700">
|
||||||
<p><span class="text-slate-500">ID:</span> {notProvided(detail.report.report_id)}</p>
|
<p><span class="text-slate-500">ID:</span> {notProvided(detail.report.report_id)}</p>
|
||||||
<p><span class="text-slate-500">Type:</span> {notProvided(detail.report.task_type)}</p>
|
<p><span class="text-slate-500">Type:</span> {notProvided(detail.report.task_type)}</p>
|
||||||
<p><span class="text-slate-500">Status:</span> {notProvided(detail.report.status)}</p>
|
<p><span class="text-slate-500">Status:</span> {notProvided(detail.report.status)}</p>
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">Diagnostics</p>
|
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">{$t.reports?.diagnostics || 'Diagnostics'}</p>
|
||||||
<pre class="max-h-48 overflow-auto rounded bg-slate-50 p-2 text-xs text-slate-700">{JSON.stringify(detail.diagnostics || { note: $t.reports?.not_provided || 'Not provided' }, null, 2)}</pre>
|
<pre class="max-h-48 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-2 text-xs text-slate-700">{JSON.stringify(detail.diagnostics || { note: $t.reports?.not_provided || 'Not provided' }, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (detail.next_actions && detail.next_actions.length > 0) || (detail.report.error_context && detail.report.error_context.next_actions && detail.report.error_context.next_actions.length > 0)}
|
{#if (detail.next_actions && detail.next_actions.length > 0) || (detail.report.error_context && detail.report.error_context.next_actions && detail.report.error_context.next_actions.length > 0)}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">Next actions</p>
|
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">{$t.reports?.next_actions || 'Next actions'}</p>
|
||||||
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-700">
|
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-700">
|
||||||
{#each (detail.next_actions && detail.next_actions.length > 0 ? detail.next_actions : detail.report.error_context.next_actions) as action}
|
{#each (detail.next_actions && detail.next_actions.length > 0 ? detail.next_actions : detail.report.error_context.next_actions) as action}
|
||||||
<li>{action}</li>
|
<li>{action}</li>
|
||||||
|
|||||||
@@ -89,12 +89,8 @@
|
|||||||
"storage_repo_pattern": "Repository Directory Pattern",
|
"storage_repo_pattern": "Repository Directory Pattern",
|
||||||
"storage_filename_pattern": "Filename Pattern",
|
"storage_filename_pattern": "Filename Pattern",
|
||||||
"storage_preview": "Path Preview",
|
"storage_preview": "Path Preview",
|
||||||
"environments": "Superset Environments",
|
|
||||||
"env_description": "Configure Superset environments for dashboards and datasets.",
|
"env_description": "Configure Superset environments for dashboards and datasets.",
|
||||||
"env_add": "Add Environment",
|
|
||||||
"env_actions": "Actions",
|
"env_actions": "Actions",
|
||||||
"env_test": "Test",
|
|
||||||
"env_delete": "Delete",
|
|
||||||
"connections_description": "Configure database connections for data mapping.",
|
"connections_description": "Configure database connections for data mapping.",
|
||||||
"llm_description": "Configure LLM providers for dataset documentation.",
|
"llm_description": "Configure LLM providers for dataset documentation.",
|
||||||
"logging": "Logging Configuration",
|
"logging": "Logging Configuration",
|
||||||
@@ -161,8 +157,6 @@
|
|||||||
"action_migrate": "Migrate",
|
"action_migrate": "Migrate",
|
||||||
"action_backup": "Backup",
|
"action_backup": "Backup",
|
||||||
"action_commit": "Commit",
|
"action_commit": "Commit",
|
||||||
"git_status": "Git Status",
|
|
||||||
"last_task": "Last Task",
|
|
||||||
"view_task": "View task",
|
"view_task": "View task",
|
||||||
"task_running": "Running...",
|
"task_running": "Running...",
|
||||||
"task_done": "Done",
|
"task_done": "Done",
|
||||||
@@ -170,23 +164,25 @@
|
|||||||
"task_waiting": "Waiting",
|
"task_waiting": "Waiting",
|
||||||
"status_synced": "Synced",
|
"status_synced": "Synced",
|
||||||
"status_diff": "Diff",
|
"status_diff": "Diff",
|
||||||
"status_synced": "Synced",
|
|
||||||
"status_diff": "Diff",
|
|
||||||
"status_error": "Error",
|
"status_error": "Error",
|
||||||
"task_running": "Running...",
|
|
||||||
"task_done": "Done",
|
|
||||||
"task_failed": "Failed",
|
|
||||||
"task_waiting": "Waiting",
|
|
||||||
"view_task": "View task",
|
|
||||||
"empty": "No dashboards found"
|
"empty": "No dashboards found"
|
||||||
},
|
},
|
||||||
"reports": {
|
"reports": {
|
||||||
"title": "Reports",
|
"title": "Reports",
|
||||||
"empty": "No reports available.",
|
"empty": "No reports available.",
|
||||||
"filtered_empty": "No reports match your filters.",
|
"filtered_empty": "No reports match your filters.",
|
||||||
|
"loading": "Loading reports...",
|
||||||
|
"retry_load": "Retry loading",
|
||||||
|
"clear_filters": "Clear filters",
|
||||||
"unknown_type": "Other / Unknown Type",
|
"unknown_type": "Other / Unknown Type",
|
||||||
"not_provided": "Not provided",
|
"not_provided": "Not provided",
|
||||||
"view_details": "View details"
|
"view_details": "View details",
|
||||||
|
"diagnostics": "Diagnostics",
|
||||||
|
"next_actions": "Next actions",
|
||||||
|
"status_success": "Success",
|
||||||
|
"status_failed": "Failed",
|
||||||
|
"status_in_progress": "In progress",
|
||||||
|
"status_partial": "Partial"
|
||||||
},
|
},
|
||||||
"datasets": {
|
"datasets": {
|
||||||
"empty": "No datasets found",
|
"empty": "No datasets found",
|
||||||
|
|||||||
@@ -89,12 +89,8 @@
|
|||||||
"storage_repo_pattern": "Шаблон директории репозиториев",
|
"storage_repo_pattern": "Шаблон директории репозиториев",
|
||||||
"storage_filename_pattern": "Шаблон имени файла",
|
"storage_filename_pattern": "Шаблон имени файла",
|
||||||
"storage_preview": "Предпросмотр пути",
|
"storage_preview": "Предпросмотр пути",
|
||||||
"environments": "Окружения Superset",
|
|
||||||
"env_description": "Настройка окружений Superset для дашбордов и датасетов.",
|
"env_description": "Настройка окружений Superset для дашбордов и датасетов.",
|
||||||
"env_add": "Добавить окружение",
|
|
||||||
"env_actions": "Действия",
|
"env_actions": "Действия",
|
||||||
"env_test": "Тест",
|
|
||||||
"env_delete": "Удалить",
|
|
||||||
"connections_description": "Настройка подключений к базам данных для маппинга.",
|
"connections_description": "Настройка подключений к базам данных для маппинга.",
|
||||||
"llm_description": "Настройка LLM провайдеров для документирования датасетов.",
|
"llm_description": "Настройка LLM провайдеров для документирования датасетов.",
|
||||||
"logging": "Настройка логирования",
|
"logging": "Настройка логирования",
|
||||||
@@ -160,8 +156,6 @@
|
|||||||
"action_migrate": "Мигрировать",
|
"action_migrate": "Мигрировать",
|
||||||
"action_backup": "Создать бэкап",
|
"action_backup": "Создать бэкап",
|
||||||
"action_commit": "Зафиксировать",
|
"action_commit": "Зафиксировать",
|
||||||
"git_status": "Статус Git",
|
|
||||||
"last_task": "Последняя задача",
|
|
||||||
"view_task": "Просмотреть задачу",
|
"view_task": "Просмотреть задачу",
|
||||||
"task_running": "Выполняется...",
|
"task_running": "Выполняется...",
|
||||||
"task_done": "Готово",
|
"task_done": "Готово",
|
||||||
@@ -169,23 +163,25 @@
|
|||||||
"task_waiting": "Ожидание",
|
"task_waiting": "Ожидание",
|
||||||
"status_synced": "Синхронизировано",
|
"status_synced": "Синхронизировано",
|
||||||
"status_diff": "Различия",
|
"status_diff": "Различия",
|
||||||
"status_synced": "Синхронизировано",
|
|
||||||
"status_diff": "Различия",
|
|
||||||
"status_error": "Ошибка",
|
"status_error": "Ошибка",
|
||||||
"task_running": "Выполняется...",
|
|
||||||
"task_done": "Готово",
|
|
||||||
"task_failed": "Ошибка",
|
|
||||||
"task_waiting": "Ожидание",
|
|
||||||
"view_task": "Просмотреть задачу",
|
|
||||||
"empty": "Дашборды не найдены"
|
"empty": "Дашборды не найдены"
|
||||||
},
|
},
|
||||||
"reports": {
|
"reports": {
|
||||||
"title": "Отчеты",
|
"title": "Отчеты",
|
||||||
"empty": "Отчеты отсутствуют.",
|
"empty": "Отчеты отсутствуют.",
|
||||||
"filtered_empty": "Нет отчетов по выбранным фильтрам.",
|
"filtered_empty": "Нет отчетов по выбранным фильтрам.",
|
||||||
|
"loading": "Загрузка отчетов...",
|
||||||
|
"retry_load": "Повторить загрузку",
|
||||||
|
"clear_filters": "Сбросить фильтры",
|
||||||
"unknown_type": "Прочее / Неизвестный тип",
|
"unknown_type": "Прочее / Неизвестный тип",
|
||||||
"not_provided": "Не указано",
|
"not_provided": "Не указано",
|
||||||
"view_details": "Подробнее"
|
"view_details": "Подробнее",
|
||||||
|
"diagnostics": "Диагностика",
|
||||||
|
"next_actions": "Следующие действия",
|
||||||
|
"status_success": "Успешно",
|
||||||
|
"status_failed": "Ошибка",
|
||||||
|
"status_in_progress": "В процессе",
|
||||||
|
"status_partial": "Частично"
|
||||||
},
|
},
|
||||||
"datasets": {
|
"datasets": {
|
||||||
"empty": "Датасеты не найдены",
|
"empty": "Датасеты не найдены",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
<Toast />
|
<Toast />
|
||||||
|
|
||||||
<main class="bg-gray-50 min-h-screen">
|
<main class="min-h-screen bg-slate-50">
|
||||||
{#if isLoginPage}
|
{#if isLoginPage}
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<slot />
|
<slot />
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page content -->
|
<!-- Page content -->
|
||||||
<div class="p-4 flex-grow">
|
<div class="flex-grow px-4 pb-6 pt-2 md:px-6">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -110,14 +110,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto max-w-6xl p-4">
|
<div class="mx-auto w-full max-w-7xl space-y-4">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={$t.reports?.title || 'Reports'}
|
title={$t.reports?.title || 'Reports'}
|
||||||
subtitle={() => null}
|
subtitle={() => null}
|
||||||
actions={() => null}
|
actions={() => null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mb-4 rounded-lg border border-slate-200 bg-white p-3">
|
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-4">
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-4">
|
||||||
<select
|
<select
|
||||||
bind:value={taskType}
|
bind:value={taskType}
|
||||||
@@ -140,14 +140,14 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="rounded-md border border-slate-200 px-3 py-1.5 text-sm hover:bg-slate-50"
|
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
on:click={() => loadReports()}
|
on:click={() => loadReports()}
|
||||||
>
|
>
|
||||||
{$t.common?.refresh || 'Refresh'}
|
{$t.common?.refresh || 'Refresh'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="rounded-md border border-slate-200 px-3 py-1.5 text-sm hover:bg-slate-50"
|
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||||
on:click={clearFilters}
|
on:click={clearFilters}
|
||||||
>
|
>
|
||||||
{$t.reports?.clear_filters || 'Clear filters'}
|
{$t.reports?.clear_filters || 'Clear filters'}
|
||||||
@@ -156,24 +156,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||||
{$t.common?.loading || 'Loading...'}
|
{$t.reports?.loading || 'Loading reports...'}
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
|
<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 shadow-sm">
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
<button class="mt-2 rounded border border-red-300 px-3 py-1 text-sm" on:click={() => loadReports()}>
|
<button class="mt-2 inline-flex items-center justify-center rounded-lg border border-red-300 px-3 py-1 text-sm font-medium text-red-700 transition-colors hover:bg-red-100" on:click={() => loadReports()}>
|
||||||
{$t.common?.retry || 'Retry'}
|
{$t.reports?.retry_load || $t.common?.retry || 'Retry'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if !collection || collection.total === 0}
|
{:else if !collection || collection.total === 0}
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||||
{$t.reports?.empty || 'No reports available.'}
|
{$t.reports?.empty || 'No reports available.'}
|
||||||
</div>
|
</div>
|
||||||
{:else if collection.items.length === 0 && hasActiveFilters()}
|
{:else if collection.items.length === 0 && hasActiveFilters()}
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||||
<p>{$t.reports?.filtered_empty || 'No reports match your filters.'}</p>
|
<p>{$t.reports?.filtered_empty || 'No reports match your filters.'}</p>
|
||||||
<button class="mt-2 rounded border border-slate-200 px-3 py-1 text-sm hover:bg-slate-50" on:click={clearFilters}>
|
<button class="mt-2 inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-1 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50" on:click={clearFilters}>
|
||||||
{$t.reports?.clear_filters || 'Clear filters'}
|
{$t.reports?.clear_filters || 'Clear filters'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -187,14 +187,14 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 py-6">
|
<div class="mx-auto w-full max-w-7xl space-y-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">
|
<h1 class="text-2xl font-bold text-gray-900">
|
||||||
{$t.settings?.title || "Settings"}
|
{$t.settings?.title || "Settings"}
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
|
class="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-hover"
|
||||||
on:click={loadSettings}
|
on:click={loadSettings}
|
||||||
>
|
>
|
||||||
{$t.common?.refresh || "Refresh"}
|
{$t.common?.refresh || "Refresh"}
|
||||||
@@ -218,12 +218,14 @@
|
|||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="bg-white rounded-lg p-6 border border-gray-200">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
<div class="space-y-3">
|
||||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||||||
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
|
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||||||
|
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if settings}
|
{:else if settings}
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
@@ -271,7 +273,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Content -->
|
<!-- Tab Content -->
|
||||||
<div class="bg-white rounded-lg p-6 border border-gray-200">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
{#if activeTab === "environments"}
|
{#if activeTab === "environments"}
|
||||||
<!-- Environments Tab -->
|
<!-- Environments Tab -->
|
||||||
<div class="text-lg font-medium mb-4">
|
<div class="text-lg font-medium mb-4">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
42
specs/001-unify-frontend-style/checklists/requirements.md
Normal file
42
specs/001-unify-frontend-style/checklists/requirements.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Specification Quality Checklist: Frontend Style Unification
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-02-23
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## UX Consistency
|
||||||
|
|
||||||
|
- [x] Functional requirements fully support the 'Happy Path' in ux_reference.md
|
||||||
|
- [x] Error handling requirements match the 'Error Experience' in ux_reference.md
|
||||||
|
- [x] No requirements contradict the defined User Persona or Context
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Validation iteration: 1
|
||||||
|
- Result: PASS (all checklist items complete)
|
||||||
|
- Specification is ready for planning workflow.
|
||||||
70
specs/001-unify-frontend-style/contracts/modules.md
Normal file
70
specs/001-unify-frontend-style/contracts/modules.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Module Contracts: Frontend Style Unification
|
||||||
|
|
||||||
|
## [DEF:FrontendStyleSystem:Module]
|
||||||
|
@TIER: CRITICAL
|
||||||
|
@PURPOSE: Define and enforce unified visual primitives and page-shell rules across targeted frontend routes.
|
||||||
|
@RELATION: DEPENDS_ON -> [DEF:Std:UI_Svelte]
|
||||||
|
@RELATION: DEPENDS_ON -> [DEF:Std:Semantics]
|
||||||
|
@RELATION: BINDS_TO -> [DEF:StyleTokenGroup:Entity]
|
||||||
|
@RELATION: BINDS_TO -> [DEF:UIPatternRule:Entity]
|
||||||
|
@PRE: Target routes/components for unification are identified and included in scope.
|
||||||
|
@PRE: Existing behavior-critical user flows remain available for validation.
|
||||||
|
@POST: Shared visual primitives and shell patterns are applied consistently in targeted scope.
|
||||||
|
@POST: No critical functional flow is removed by style refactor.
|
||||||
|
@UX_STATE: Default -> Users see consistent hierarchy (title/actions/content) across targeted pages.
|
||||||
|
@UX_STATE: Loading -> Loading visuals appear in consistent zones without disruptive layout jumps.
|
||||||
|
@UX_STATE: Error -> Error blocks use consistent emphasis and include a clear recovery action.
|
||||||
|
@UX_STATE: Success -> Confirmation messages follow one tone/placement pattern.
|
||||||
|
@INVARIANT: Unified styling must not regress accessibility-visible focus and readable contrast behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [DEF:RouteShellContract:Component]
|
||||||
|
@TIER: CRITICAL
|
||||||
|
@PURPOSE: Standardize route shell structure for primary pages (dashboards, tasks, reports, settings).
|
||||||
|
@RELATION: DEPENDS_ON -> [DEF:FrontendStyleSystem:Module]
|
||||||
|
@PRE: Route provides title context and action area metadata.
|
||||||
|
@POST: Route renders canonical shell order: context/breadcrumbs, title block, action region, content container.
|
||||||
|
@UX_STATE: Default -> Primary action location is discoverable quickly and consistently.
|
||||||
|
@UX_STATE: Empty -> Empty-state container is visually aligned with shell and includes next-step guidance.
|
||||||
|
@UX_RECOVERY: Empty -> User can recover using explicit action (refresh/filter adjust/create flow).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [DEF:StateFeedbackContract:Component]
|
||||||
|
@TIER: CRITICAL
|
||||||
|
@PURPOSE: Normalize loading/empty/error/success feedback rendering and wording across modules.
|
||||||
|
@RELATION: DEPENDS_ON -> [DEF:StatePresentationPattern:Entity]
|
||||||
|
@PRE: Module can expose current state category (loading/empty/error/success).
|
||||||
|
@POST: State-specific UI uses canonical placement, tone, and recovery behavior.
|
||||||
|
@UX_STATE: Loading -> Consistent indicator style and placement with stable layout rhythm.
|
||||||
|
@UX_STATE: Empty -> Neutral message + guidance action rendered in standard block.
|
||||||
|
@UX_STATE: Error -> Actionable error messaging with retry/fix path.
|
||||||
|
@UX_STATE: Success -> Concise confirmation in standard visual language.
|
||||||
|
@UX_FEEDBACK: Error -> Emphasis clearly distinguishes failure from neutral status.
|
||||||
|
@UX_RECOVERY: Error -> Retry or corrective action is always visible when recoverable.
|
||||||
|
@INVARIANT: State texts use canonical terminology and remain i18n-compatible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [DEF:TerminologyConsistencyContract:Module]
|
||||||
|
@TIER: STANDARD
|
||||||
|
@PURPOSE: Keep user-facing wording consistent across page shells and state blocks.
|
||||||
|
@RELATION: DEPENDS_ON -> [DEF:Std:Constitution]
|
||||||
|
@PRE: Canonical term list for targeted flows is defined.
|
||||||
|
@POST: Targeted modules avoid mixed synonyms for the same concept.
|
||||||
|
@UX_STATE: Default -> UI labels and status texts remain concise and confidence-building.
|
||||||
|
@INVARIANT: User-facing text remains compatible with existing localization workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contract Usage Simulation (Key Scenario)
|
||||||
|
|
||||||
|
**Scenario**: User navigates from dashboards to reports and encounters a failed data load.
|
||||||
|
|
||||||
|
1. `RouteShellContract` ensures both pages keep identical shell rhythm and action placement.
|
||||||
|
2. `FrontendStyleSystem` ensures shared primitives (spacing/typography/cards/buttons) are consistent.
|
||||||
|
3. `StateFeedbackContract` renders failure using canonical error block with explicit retry action.
|
||||||
|
4. `TerminologyConsistencyContract` ensures error wording and action labels are aligned across pages.
|
||||||
|
|
||||||
|
**Continuity Check**: No interface mismatch detected between shell-level structure and state-level feedback contracts.
|
||||||
119
specs/001-unify-frontend-style/data-model.md
Normal file
119
specs/001-unify-frontend-style/data-model.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Data Model: Frontend Style Unification
|
||||||
|
|
||||||
|
## Entity: StyleTokenGroup
|
||||||
|
|
||||||
|
**Purpose**: Canonical set of visual decisions reused across routes/components.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- `token_group_id` (string, required): Unique identifier of token group.
|
||||||
|
- `name` (string, required): Human-readable token group name.
|
||||||
|
- `typography_roles` (list, required): Named typography levels (e.g., page-title, section-title, body, helper).
|
||||||
|
- `spacing_scale` (list, required): Ordered spacing steps used by layout/components.
|
||||||
|
- `color_roles` (list, required): Semantic color roles (primary action, warning, error emphasis, neutral content).
|
||||||
|
- `shape_rules` (list, required): Corner/radius and border behavior rules.
|
||||||
|
- `status` (enum, required): `draft | active | deprecated`.
|
||||||
|
|
||||||
|
### Validation Rules
|
||||||
|
|
||||||
|
- Must contain at least one typography role, spacing step, and color role.
|
||||||
|
- `active` token groups must not contain conflicting role names.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity: UIPatternRule
|
||||||
|
|
||||||
|
**Purpose**: Reusable contract for structural/interactive patterns.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- `pattern_id` (string, required): Unique pattern identifier.
|
||||||
|
- `pattern_type` (enum, required): `page-shell | action-bar | card | form-section | state-block`.
|
||||||
|
- `target_scope` (list, required): Routes or component families where rule applies.
|
||||||
|
- `layout_requirements` (list, required): Structural rules (placement, grouping, spacing).
|
||||||
|
- `interaction_requirements` (list, required): Behavior/state requirements.
|
||||||
|
- `exception_policy` (string, required): Allowed deviation rules.
|
||||||
|
- `status` (enum, required): `proposed | approved | retired`.
|
||||||
|
|
||||||
|
### Validation Rules
|
||||||
|
|
||||||
|
- Each approved pattern must map to at least one scope item.
|
||||||
|
- Pattern must define at least one layout and one interaction requirement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity: StatePresentationPattern
|
||||||
|
|
||||||
|
**Purpose**: Canonical representation for loading/empty/error/success feedback.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- `state_pattern_id` (string, required): Unique state pattern identifier.
|
||||||
|
- `state_type` (enum, required): `loading | empty | success | error`.
|
||||||
|
- `message_tone_rule` (string, required): Tone/voice constraint for user text.
|
||||||
|
- `placement_rule` (string, required): Where state is shown in page/component layout.
|
||||||
|
- `recovery_action_rule` (string, optional): Recovery expectations (e.g., retry, fix input).
|
||||||
|
- `accessibility_notes` (list, required): Focus/contrast/readability constraints.
|
||||||
|
- `i18n_rule` (string, required): Localization requirement for state text.
|
||||||
|
|
||||||
|
### Validation Rules
|
||||||
|
|
||||||
|
- `error` and `empty` states should include explicit recovery guidance.
|
||||||
|
- Every state pattern must include i18n and accessibility notes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity: ConformanceScopeItem
|
||||||
|
|
||||||
|
**Purpose**: Track conformance status of a route/component group to unified style.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- `scope_item_id` (string, required): Unique scope record identifier.
|
||||||
|
- `scope_type` (enum, required): `route | component-group`.
|
||||||
|
- `scope_name` (string, required): Name of route/component area.
|
||||||
|
- `applied_token_group_id` (string, optional): Linked active token group.
|
||||||
|
- `applied_pattern_ids` (list, optional): Linked pattern IDs.
|
||||||
|
- `conformance_status` (enum, required): `not-started | partial | conformant | deferred`.
|
||||||
|
- `exception_reason` (string, optional): Why full conformance is deferred.
|
||||||
|
- `followup_action` (string, optional): Planned action for deferred areas.
|
||||||
|
- `owner` (string, optional): Responsible implementation owner/team.
|
||||||
|
- `last_review_date` (date, optional): Last conformance verification date.
|
||||||
|
|
||||||
|
### Validation Rules
|
||||||
|
|
||||||
|
- `deferred` status requires both `exception_reason` and `followup_action`.
|
||||||
|
- `conformant` status requires at least one applied token/pattern link.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
- `StyleTokenGroup` 1..N -> `ConformanceScopeItem`
|
||||||
|
A token group can apply to multiple scope items.
|
||||||
|
- `UIPatternRule` N..N -> `ConformanceScopeItem`
|
||||||
|
Multiple patterns may apply per scope item; one pattern can serve many scope items.
|
||||||
|
- `StatePresentationPattern` 1..N -> `UIPatternRule`
|
||||||
|
State patterns are referenced by pattern rules where states are rendered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Transition Notes
|
||||||
|
|
||||||
|
### ConformanceScopeItem Lifecycle
|
||||||
|
|
||||||
|
`not-started -> partial -> conformant`
|
||||||
|
`partial -> deferred` (if blocked by legacy constraints)
|
||||||
|
`deferred -> partial -> conformant` (after follow-up implementation)
|
||||||
|
|
||||||
|
### StyleTokenGroup Lifecycle
|
||||||
|
|
||||||
|
`draft -> active -> deprecated`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Volume / Scale Assumptions
|
||||||
|
|
||||||
|
- Scope tracking is bounded to targeted primary routes and shared component groups.
|
||||||
|
- Pattern/token entities are low-volume and human-curated.
|
||||||
|
- Review updates occur during iterative feature delivery, not high-frequency runtime events.
|
||||||
103
specs/001-unify-frontend-style/plan.md
Normal file
103
specs/001-unify-frontend-style/plan.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Implementation Plan: Frontend Style Unification
|
||||||
|
|
||||||
|
**Branch**: `[001-unify-frontend-style]` | **Date**: 2026-02-23 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/001-unify-frontend-style/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Unify the frontend visual system and interaction patterns across primary product routes so users experience a coherent, predictable interface.
|
||||||
|
The implementation approach is to standardize shared UI primitives and page-shell patterns first, then align navigation and state feedback patterns, while preserving existing behavior and i18n/accessibility constraints. UX consistency in `ux_reference.md` is treated as a hard requirement and mapped to concrete component contracts and verification steps.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: Node.js 18+ runtime, SvelteKit (existing frontend stack)
|
||||||
|
**Primary Dependencies**: SvelteKit, Tailwind CSS, existing frontend UI primitives under `frontend/src/lib/components/ui`
|
||||||
|
**Storage**: N/A (UI styling and component behavior only)
|
||||||
|
**Testing**: Vitest + existing frontend component/store tests
|
||||||
|
**Target Platform**: Web browser (desktop-first internal product UI)
|
||||||
|
**Project Type**: Web application (frontend + backend repository, frontend-focused scope)
|
||||||
|
**Performance Goals**: Preserve existing perceived responsiveness; avoid layout shifts in loading/error/success/empty states
|
||||||
|
**Constraints**:
|
||||||
|
- Must follow Tailwind-first styling and avoid introducing native `fetch` usage (Constitution)
|
||||||
|
- Must keep user-facing text compatible with existing i18n strategy
|
||||||
|
- Must not regress core task flows while refactoring UI
|
||||||
|
- Must preserve accessibility-visible focus and readable contrast behavior
|
||||||
|
**Scale/Scope**: Core primary routes and shared layout/components used by dashboards, tasks, reports, settings
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
- ✅ **Semantic Protocol Compliance**: Planned artifacts include contracts with `[DEF]`/`@TIER`/`@PRE`/`@POST`/`@UX_STATE` for affected modules in `contracts/modules.md`.
|
||||||
|
- ✅ **Unified Frontend Experience**: Scope is frontend style unification with Tailwind-first constraints; i18n consistency explicitly included.
|
||||||
|
- ✅ **Independent Testability**: Spec includes independent tests per user story; quickstart includes isolated verification flows.
|
||||||
|
- ✅ **Architecture Integrity**: No plugin or backend execution model changes required; frontend-only structural alignment.
|
||||||
|
- ⚠️ **Known Repository Risk (External to this feature)**: Multiple `specs/001-*` directories exist in repo and trigger script warnings. Feature plan continues with explicit active feature directory `specs/001-unify-frontend-style`.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/001-unify-frontend-style/
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── modules.md
|
||||||
|
└── tasks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/
|
||||||
|
└── src/
|
||||||
|
├── api/
|
||||||
|
├── models/
|
||||||
|
└── services/
|
||||||
|
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ │ ├── layout/
|
||||||
|
│ │ │ ├── reports/
|
||||||
|
│ │ │ └── ui/
|
||||||
|
│ │ ├── stores/
|
||||||
|
│ │ └── i18n/
|
||||||
|
│ ├── components/
|
||||||
|
│ └── routes/
|
||||||
|
└── tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Use existing web-application layout and implement changes primarily in `frontend/src/lib/components`, `frontend/src/routes`, and related frontend tests.
|
||||||
|
|
||||||
|
## Phase 0: Research Focus
|
||||||
|
|
||||||
|
1. Standardize style baseline strategy for existing Tailwind-based Svelte components.
|
||||||
|
2. Define migration strategy for legacy/non-conformant components without behavior regression.
|
||||||
|
3. Define UX-consistent state patterns (loading/empty/error/success) reusable across pages.
|
||||||
|
4. Define i18n and accessibility safeguards during style unification.
|
||||||
|
5. Define validation approach (visual conformance + behavior safety checks).
|
||||||
|
|
||||||
|
## Phase 1: Design & Contracts Outputs
|
||||||
|
|
||||||
|
- Produce `data-model.md` with style/token/pattern/conformance entities.
|
||||||
|
- Produce `contracts/modules.md` with semantic contracts and UX states for critical modules.
|
||||||
|
- Produce `quickstart.md` with executable validation steps for independent scenario checks.
|
||||||
|
- Re-run Constitution check after design to confirm no UX compromise.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
No constitution violations requiring explicit exceptions.
|
||||||
|
|
||||||
|
## Test Data Reference
|
||||||
|
|
||||||
|
| Component | TIER | Fixture Name | Location |
|
||||||
|
|-----------|------|--------------|----------|
|
||||||
|
| FrontendStyleReview | CRITICAL | style_review_sample | spec.md#test-data-fixtures |
|
||||||
|
| StateFeedbackPattern | CRITICAL | state_feedback_sample | spec.md#test-data-fixtures |
|
||||||
|
|
||||||
|
**Note**: Tester Agent MUST use these fixtures when writing unit/integration tests for CRITICAL modules.
|
||||||
72
specs/001-unify-frontend-style/quickstart.md
Normal file
72
specs/001-unify-frontend-style/quickstart.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Quickstart: Frontend Style Unification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Validate that frontend style unification is implemented consistently across targeted routes without functional regressions.
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- Feature branch: `001-unify-frontend-style`
|
||||||
|
- Frontend dependencies installed
|
||||||
|
- Application starts successfully in local environment
|
||||||
|
- Target routes available: dashboards, tasks, reports, settings
|
||||||
|
|
||||||
|
## Validation Flow
|
||||||
|
|
||||||
|
### 1) User Story 1 — Consistent Visual Foundation (P1)
|
||||||
|
|
||||||
|
1. Open each target route: dashboards, tasks, reports, settings.
|
||||||
|
2. Compare visual primitives:
|
||||||
|
- typography hierarchy
|
||||||
|
- spacing rhythm
|
||||||
|
- card/container style
|
||||||
|
- button variant consistency
|
||||||
|
3. Confirm no route has conflicting visual language for shared primitives.
|
||||||
|
|
||||||
|
**Expected Result**: Shared visual baseline appears coherent across all targeted routes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2) User Story 2 — Unified Navigation and Page Shells (P2)
|
||||||
|
|
||||||
|
1. Navigate through at least three top-level routes.
|
||||||
|
2. Verify shell consistency:
|
||||||
|
- page title placement
|
||||||
|
- context/breadcrumb area behavior
|
||||||
|
- primary/secondary action region location
|
||||||
|
- content container alignment
|
||||||
|
3. Confirm transitions do not break orientation cues.
|
||||||
|
|
||||||
|
**Expected Result**: Page shell pattern is consistent and predictable across routes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3) User Story 3 — Predictable Feedback and States (P3)
|
||||||
|
|
||||||
|
1. Trigger loading state on at least two routes.
|
||||||
|
2. Trigger empty state and error state where possible.
|
||||||
|
3. Trigger one success feedback event (save/update/apply action).
|
||||||
|
4. Compare message tone, placement, and recovery actions.
|
||||||
|
|
||||||
|
**Expected Result**: Loading/empty/error/success feedback follows one canonical pattern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regression Safety Checks
|
||||||
|
|
||||||
|
1. Execute core functional flows on dashboards/tasks/reports/settings.
|
||||||
|
2. Confirm style unification did not remove or alter business-critical actions.
|
||||||
|
3. Confirm focus visibility and readability remain acceptable on updated UI areas.
|
||||||
|
|
||||||
|
**Expected Result**: No critical user flow regression and no accessibility degradation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conformance Checklist Snapshot
|
||||||
|
|
||||||
|
- [ ] Shared primitives aligned in targeted scope
|
||||||
|
- [ ] Page shell contract honored
|
||||||
|
- [ ] State feedback contract honored
|
||||||
|
- [ ] Terminology/tone consistency preserved
|
||||||
|
- [ ] i18n-compatible user-facing text
|
||||||
|
- [ ] No critical flow regressions
|
||||||
87
specs/001-unify-frontend-style/research.md
Normal file
87
specs/001-unify-frontend-style/research.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Research: Frontend Style Unification
|
||||||
|
|
||||||
|
## Decision 1: Tailwind-first unification through shared primitives and layout patterns
|
||||||
|
|
||||||
|
**Decision**: Use existing shared UI components and route-level layout patterns as the primary unification mechanism, with Tailwind utility classes as the style source of truth.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Aligns with constitution requirement: Tailwind-first and minimal scoped styles.
|
||||||
|
- Reduces risk versus page-by-page ad-hoc class rewrites.
|
||||||
|
- Enables predictable rollout and easier review by centralizing style behavior.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Big-bang rewrite of all pages in one pass (rejected: high regression risk).
|
||||||
|
- Introducing a second styling abstraction layer (rejected: added complexity and drift).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 2: Incremental conformance with explicit exception registry
|
||||||
|
|
||||||
|
**Decision**: Apply style unification incrementally across core routes; for non-conformant legacy widgets, use documented fallback styles and track exceptions for follow-up.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Preserves functional behavior while raising consistency quickly.
|
||||||
|
- Supports FR-005/FR-006 in spec by preventing disruption of critical flows.
|
||||||
|
- Makes scope and technical debt explicit.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Block release until 100% conformance (rejected: delays value delivery).
|
||||||
|
- Ignore non-conformant areas (rejected: no governance and unresolved inconsistency).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 3: Canonical UX state patterns for loading/empty/error/success
|
||||||
|
|
||||||
|
**Decision**: Define one reusable state pattern per state type (layout placement, message format, recovery action position) and apply to targeted modules.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Directly supports UX reference and SC-003.
|
||||||
|
- Improves predictability and user trust.
|
||||||
|
- Simplifies QA with deterministic state contracts.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Module-specific state designs (rejected: reintroduces inconsistency).
|
||||||
|
- Visual-only alignment without message/rule alignment (rejected: incomplete UX consistency).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 4: i18n and terminology normalization as part of style work
|
||||||
|
|
||||||
|
**Decision**: Include text tone/terminology consistency in scope for user-facing state and action labels; avoid hardcoded strings during updates.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Required by constitution i18n rule.
|
||||||
|
- Prevents mixed terms after visual unification.
|
||||||
|
- Supports FR-007 and UX tone requirements.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Deferring terminology to a separate feature (rejected: causes visible inconsistency after style updates).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 5: Accessibility-preserving visual alignment
|
||||||
|
|
||||||
|
**Decision**: Keep focus visibility and readable contrast as non-negotiable constraints; when style and accessibility conflict, accessibility wins.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Matches edge-case requirements in spec.
|
||||||
|
- Reduces user risk and supports sustainable UI governance.
|
||||||
|
- Prevents regressions masked by purely visual approvals.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Prioritizing strict visual sameness in all cases (rejected: can degrade accessibility outcomes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 6: Verification model = conformance checklist + route smoke tests + UX state checks
|
||||||
|
|
||||||
|
**Decision**: Validate through structured cross-route conformance checks, independent user-story tests, and targeted UX state verification.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Produces measurable evidence for SC-001..SC-005.
|
||||||
|
- Aligns with independent testability principle in constitution.
|
||||||
|
- Keeps verification technology-agnostic at feature level while staying executable in project context.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Pure visual review without scenario checks (rejected: misses behavior regressions).
|
||||||
|
- Full end-to-end redesign QA cycle before incremental rollout (rejected: too heavy for initial unification phase).
|
||||||
131
specs/001-unify-frontend-style/spec.md
Normal file
131
specs/001-unify-frontend-style/spec.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Feature Specification: Frontend Style Unification
|
||||||
|
|
||||||
|
**Feature Branch**: `[001-unify-frontend-style]`
|
||||||
|
**Reference UX**: `[ux_reference.md]` (See specific folder)
|
||||||
|
**Created**: 2026-02-23
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Даю тебе полный кардбланш на приведение фронтэнда к единому стилю. Прочитай .ai/ROOT.md, используй всю методологию workflow speckit при разработке. Вперед"
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Consistent Visual Foundation (Priority: P1)
|
||||||
|
|
||||||
|
As a product user, I want all major screens and shared UI blocks to look and behave consistently so that the interface feels coherent and predictable.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core business value of the request. Without a shared visual baseline, every other UX improvement remains fragmented.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by opening key routes (dashboards, tasks, reports, settings) and confirming that shared UI primitives (spacing, typography hierarchy, cards, buttons, states) are visually consistent and predictable.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a user opens two different primary routes, **When** they compare page structure and shared controls, **Then** they see the same spacing rhythm, typography hierarchy, and interaction patterns.
|
||||||
|
2. **Given** a user interacts with common controls (buttons, tabs, cards), **When** the controls are viewed across different pages, **Then** they have consistent style variants and state behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Unified Navigation and Page Shells (Priority: P2)
|
||||||
|
|
||||||
|
As a user navigating between sections, I want navigation, headers, and content shells to follow one pattern so that I can orient quickly and reduce navigation errors.
|
||||||
|
|
||||||
|
**Why this priority**: Once visual foundation exists, navigation consistency delivers immediate usability gains and lowers cognitive load.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested independently by traversing main navigation paths and confirming shell consistency (title placement, breadcrumbs behavior, action region layout, content container behavior).
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a user switches between at least three top-level pages, **When** each page loads, **Then** page shell structure (title, context, actions, content container) follows one standard pattern.
|
||||||
|
2. **Given** a user relies on breadcrumbs and navigation cues, **When** they move deeper and back in the hierarchy, **Then** navigation cues remain consistent and unambiguous.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Predictable Feedback and States (Priority: P3)
|
||||||
|
|
||||||
|
As a user performing actions, I want loading, empty, success, and error states to be consistent across modules so that I always understand system status and next steps.
|
||||||
|
|
||||||
|
**Why this priority**: Consistent state feedback improves trust and task completion, but can be delivered after foundational visual and navigation unification.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by triggering common states (loading data, empty results, validation errors, successful actions) on selected modules and verifying consistent tone, placement, and recovery guidance.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a user opens a page with delayed data, **When** loading is shown, **Then** loading indicators follow one standard style and placement pattern.
|
||||||
|
2. **Given** a user encounters an empty or error state, **When** the state appears, **Then** the message format and recovery action style are consistent with other modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when legacy components cannot be visually aligned without breaking current behavior?
|
||||||
|
The system keeps behavior intact while applying the closest approved style fallback and records the component for deferred refinement.
|
||||||
|
- How does system handle mixed localized text lengths affecting unified layout?
|
||||||
|
Layout rules must preserve readability and alignment under longer labels and translated text.
|
||||||
|
- What happens when a page includes highly custom data widgets that do not match standard containers?
|
||||||
|
The page still applies shared shell and spacing rules while allowing controlled exceptions for specialized content blocks.
|
||||||
|
- How does system handle accessibility-related differences (focus ring, contrast expectations) during unification?
|
||||||
|
Accessibility-preserving variants take precedence over purely decorative alignment.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: System MUST define and apply a single frontend style baseline for typography hierarchy, spacing rhythm, color usage roles, and corner/radius behavior across primary user-facing pages.
|
||||||
|
- **FR-002**: System MUST standardize shared component presentation patterns for page headers, cards, buttons, form sections, and content containers.
|
||||||
|
- **FR-003**: Users MUST be able to navigate between core sections and observe consistent page shell structure, including title region, contextual navigation cues, and action placement.
|
||||||
|
- **FR-004**: System MUST provide consistent state presentation rules for loading, empty, success, and error states in all targeted modules.
|
||||||
|
- **FR-005**: System MUST preserve existing functional behavior while applying style unification; visual refactoring must not remove or alter core task flows.
|
||||||
|
- **FR-006**: System MUST define explicit exception handling rules for modules/components that cannot fully conform immediately, including fallback styling and documented follow-up actions.
|
||||||
|
- **FR-007**: System MUST align user-facing text tone and terminology in UI status messages to a unified voice and naming pattern.
|
||||||
|
- **FR-008**: System MUST ensure unified styling remains compatible with existing localization and accessibility expectations (focus visibility, readable contrast, scalable layout for longer labels).
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Style Token Group**: A logical definition of visual decisions (e.g., typography roles, spacing steps, semantic color roles, shape rules) used to enforce consistent appearance.
|
||||||
|
- **UI Pattern Rule**: A reusable standard for structural or interaction patterns (e.g., page shell, action bar, card section, state message block).
|
||||||
|
- **State Presentation Pattern**: A canonical rendering and messaging rule for loading, empty, success, and error states.
|
||||||
|
- **Conformance Scope Item**: A mapped frontend area (route/component group) with current conformance status, expected target pattern, and exception note if needed.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: At least 90% of targeted primary frontend pages conform to the approved style baseline in a structured review checklist.
|
||||||
|
- **SC-002**: Users can identify page title, navigation cue, and primary action location on targeted pages in under 5 seconds during UX validation walkthroughs.
|
||||||
|
- **SC-003**: In cross-page UX review, at least 95% of sampled loading/empty/error/success states follow the same message and layout conventions.
|
||||||
|
- **SC-004**: At least 80% of internal reviewers rate the updated UI as “visually consistent” or better after unification.
|
||||||
|
- **SC-005**: No critical user flow regressions are introduced in the set of core routes covered by the style unification scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Data Fixtures *(recommended for CRITICAL components)*
|
||||||
|
|
||||||
|
### Fixtures
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
style_review_sample:
|
||||||
|
description: "Representative result set for style conformance review"
|
||||||
|
data:
|
||||||
|
pages_checked:
|
||||||
|
- dashboards
|
||||||
|
- tasks
|
||||||
|
- reports
|
||||||
|
- settings
|
||||||
|
conformance_summary:
|
||||||
|
fully_conformant: 3
|
||||||
|
partially_conformant: 1
|
||||||
|
deferred_exceptions: 1
|
||||||
|
|
||||||
|
state_feedback_sample:
|
||||||
|
description: "Representative UI state messages for consistency checks"
|
||||||
|
data:
|
||||||
|
loading_state:
|
||||||
|
message: "Loading data…"
|
||||||
|
action: null
|
||||||
|
empty_state:
|
||||||
|
message: "No items found"
|
||||||
|
action: "Refresh or adjust filters"
|
||||||
|
error_state:
|
||||||
|
message: "Unable to load data"
|
||||||
|
action: "Retry"
|
||||||
|
success_state:
|
||||||
|
message: "Changes saved"
|
||||||
|
action: null
|
||||||
138
specs/001-unify-frontend-style/tasks.md
Normal file
138
specs/001-unify-frontend-style/tasks.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Tasks: Frontend Style Unification
|
||||||
|
|
||||||
|
**Input**: Design docs from `specs/001-unify-frontend-style/`
|
||||||
|
**Prerequisites**: plan.md, spec.md, ux_reference.md, research.md, data-model.md, contracts/modules.md, quickstart.md
|
||||||
|
|
||||||
|
## Phase 1: Setup (Project Initialization)
|
||||||
|
|
||||||
|
- [ ] T001 Define style-unification scope matrix for target routes/components in specs/001-unify-frontend-style/quickstart.md
|
||||||
|
- [ ] T002 Create conformance review checklist baseline in specs/001-unify-frontend-style/quickstart.md
|
||||||
|
- [ ] T003 Prepare implementation notes for exception handling workflow in specs/001-unify-frontend-style/research.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
- [ ] T004 Define canonical page-shell pattern and shared layout rules in frontend/src/lib/components/layout/PageHeader.svelte
|
||||||
|
- [ ] T005 [P] Define standardized card/container pattern alignment in frontend/src/lib/components/ui/Card.svelte
|
||||||
|
- [ ] T006 [P] Define standardized action/button hierarchy rules in frontend/src/lib/components/ui/Button.svelte
|
||||||
|
- [ ] T007 Define canonical state feedback pattern (loading/empty/error/success) in frontend/src/lib/components/ui/StateBlock.svelte
|
||||||
|
- [ ] T008 Align core shared terminology keys for UI statuses/actions in frontend/src/lib/i18n/index.ts
|
||||||
|
- [ ] T009 Document deferred exceptions tracking template in specs/001-unify-frontend-style/data-model.md
|
||||||
|
|
||||||
|
**Checkpoint**: Foundational style primitives and contracts are in place; user-story delivery can proceed independently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Consistent Visual Foundation (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Unify visual primitives and shared UI presentation across key pages.
|
||||||
|
|
||||||
|
**Independent Test**: Open dashboards/tasks/reports/settings and verify typography, spacing, card/container style, and buttons follow one baseline.
|
||||||
|
|
||||||
|
- [ ] T010 [US1] Apply unified visual baseline to dashboards page structure in frontend/src/routes/dashboards/+page.svelte
|
||||||
|
- [ ] T011 [US1] Apply unified visual baseline to tasks page structure in frontend/src/routes/tasks/+page.svelte
|
||||||
|
- [ ] T012 [US1] Apply unified visual baseline to reports page structure in frontend/src/routes/reports/+page.svelte
|
||||||
|
- [ ] T013 [US1] Apply unified visual baseline to settings page structure in frontend/src/routes/settings/+page.svelte
|
||||||
|
- [ ] T014 [P] [US1] Refactor shared card usage to canonical container rules in frontend/src/lib/components/reports/ReportCard.svelte
|
||||||
|
- [ ] T015 [P] [US1] Refactor shared input/control spacing to baseline rules in frontend/src/lib/components/ui/Input.svelte
|
||||||
|
- [ ] T016 [US1] Preserve functional behavior while applying visual refactor in frontend/src/routes/tasks/+page.svelte
|
||||||
|
- [ ] T017 [US1] Verify implementation matches ux_reference.md (Happy Path & Errors) in specs/001-unify-frontend-style/ux_reference.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Unified Navigation and Page Shells (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Standardize shell/navigation structure so orientation is predictable across sections.
|
||||||
|
|
||||||
|
**Independent Test**: Navigate across at least three top-level pages and confirm consistent shell hierarchy and action placement.
|
||||||
|
|
||||||
|
- [ ] T018 [US2] Standardize top shell layout behavior for navigation/title/action regions in frontend/src/lib/components/layout/TopNavbar.svelte
|
||||||
|
- [ ] T019 [US2] Standardize breadcrumbs pattern and hierarchy rendering in frontend/src/lib/components/layout/Breadcrumbs.svelte
|
||||||
|
- [ ] T020 [US2] Align sidebar navigation visual/state consistency with shell patterns in frontend/src/lib/components/layout/Sidebar.svelte
|
||||||
|
- [ ] T021 [US2] Align global task drawer shell integration with unified layout rhythm in frontend/src/lib/components/layout/TaskDrawer.svelte
|
||||||
|
- [ ] T022 [P] [US2] Align dashboards route shell contract usage in frontend/src/routes/dashboards/+page.svelte
|
||||||
|
- [ ] T023 [P] [US2] Align tasks route shell contract usage in frontend/src/routes/tasks/+page.svelte
|
||||||
|
- [ ] T024 [P] [US2] Align reports route shell contract usage in frontend/src/routes/reports/+page.svelte
|
||||||
|
- [ ] T025 [US2] Verify implementation matches ux_reference.md (Happy Path & Errors) in specs/001-unify-frontend-style/ux_reference.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Predictable Feedback and States (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Ensure loading/empty/error/success feedback is consistent in style, tone, and recovery guidance.
|
||||||
|
|
||||||
|
**Independent Test**: Trigger state feedback on targeted modules and confirm canonical message/placement/recovery consistency.
|
||||||
|
|
||||||
|
- [ ] T026 [US3] Implement canonical loading/empty/error/success blocks for reports experience in frontend/src/routes/reports/+page.svelte
|
||||||
|
- [ ] T027 [US3] Implement canonical loading/empty/error/success blocks for tasks experience in frontend/src/routes/tasks/+page.svelte
|
||||||
|
- [ ] T028 [US3] Align report detail feedback states to canonical patterns in frontend/src/lib/components/reports/ReportDetailPanel.svelte
|
||||||
|
- [ ] T029 [P] [US3] Align report card status messaging and emphasis consistency in frontend/src/lib/components/reports/ReportCard.svelte
|
||||||
|
- [ ] T030 [US3] Normalize user-facing status/recovery terminology via i18n keys in frontend/src/lib/i18n/locales/en.json
|
||||||
|
- [ ] T031 [P] [US3] Normalize user-facing status/recovery terminology via i18n keys in frontend/src/lib/i18n/locales/ru.json
|
||||||
|
- [ ] T032 [US3] Verify implementation matches ux_reference.md (Happy Path & Errors) in specs/001-unify-frontend-style/ux_reference.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Phase: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
- [ ] T033 Run route-level visual conformance walkthrough and update checklist in specs/001-unify-frontend-style/quickstart.md
|
||||||
|
- [ ] T034 [P] Verify no critical flow regressions on dashboards/tasks/reports/settings in frontend/src/routes/dashboards/+page.svelte
|
||||||
|
- [ ] T035 [P] Verify accessibility-visible focus/contrast constraints on updated components in frontend/src/lib/components/ui/Button.svelte
|
||||||
|
- [ ] T036 Update deferred exceptions and follow-up actions in specs/001-unify-frontend-style/data-model.md
|
||||||
|
- [ ] T037 Finalize implementation notes and readiness summary in specs/001-unify-frontend-style/plan.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- Setup (Phase 1) → required before Foundational (Phase 2)
|
||||||
|
- Foundational (Phase 2) → required before all User Story phases
|
||||||
|
- US1 (Phase 3) → MVP baseline
|
||||||
|
- US2 (Phase 4) depends on foundational + US1 visual baseline
|
||||||
|
- US3 (Phase 5) depends on foundational; can proceed after shell patterns are stable
|
||||||
|
- Final Phase depends on completion of selected story scope
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (P1)**: Independent MVP slice
|
||||||
|
- **US2 (P2)**: Builds on consistent primitives from US1
|
||||||
|
- **US3 (P3)**: Can be implemented after foundational state blocks; best after US1/US2 alignment for consistency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Execution Opportunities
|
||||||
|
|
||||||
|
- T005, T006 can run in parallel after T004
|
||||||
|
- T014 and T015 can run in parallel in US1
|
||||||
|
- T022, T023, T024 can run in parallel in US2
|
||||||
|
- T029, T031 can run in parallel in US3
|
||||||
|
- T034 and T035 can run in parallel in Final Phase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (US1)
|
||||||
|
|
||||||
|
1. Complete Phases 1–2
|
||||||
|
2. Deliver US1 (consistent visual foundation) with T017 UX verification
|
||||||
|
3. Validate quickstart conformance for core routes
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Add US2 shell/navigation unification + T025 verification
|
||||||
|
2. Add US3 feedback/state consistency + T032 verification
|
||||||
|
3. Complete polish and final conformance/regression checks
|
||||||
|
|
||||||
|
### Format Validation
|
||||||
|
|
||||||
|
All tasks follow required checklist format:
|
||||||
|
|
||||||
|
- `- [ ]`
|
||||||
|
- Task ID (`Txxx`)
|
||||||
|
- `[P]` marker only where parallelizable
|
||||||
|
- `[USx]` label on user-story tasks
|
||||||
|
- Explicit file path per task
|
||||||
63
specs/001-unify-frontend-style/ux_reference.md
Normal file
63
specs/001-unify-frontend-style/ux_reference.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# UX Reference: Frontend Style Unification
|
||||||
|
|
||||||
|
**Feature Branch**: `[001-unify-frontend-style]`
|
||||||
|
**Created**: 2026-02-23
|
||||||
|
**Status**: Draft
|
||||||
|
|
||||||
|
## 1. User Persona & Context
|
||||||
|
|
||||||
|
* **Who is the user?**: Product user and internal analyst who frequently switches between dashboards, tasks, reports, and settings.
|
||||||
|
* **What is their goal?**: Complete routine operations quickly in a UI that feels consistent, predictable, and trustworthy.
|
||||||
|
* **Context**: Browser-based multi-page workflow in the existing web frontend, often under time pressure, with repeated navigation between sections.
|
||||||
|
|
||||||
|
## 2. The "Happy Path" Narrative
|
||||||
|
|
||||||
|
The user opens the application and immediately recognizes where they are because every page has the same shell rhythm: clear title area, predictable action zone, and familiar content layout. As they move between dashboards, tasks, and reports, controls look and behave the same, so they do not need to relearn interactions. Loading and feedback states appear in the same visual language, which reduces hesitation and prevents mistakes. The overall experience feels coherent, fast to parse, and professionally maintained.
|
||||||
|
|
||||||
|
## 3. Interface Mockups
|
||||||
|
|
||||||
|
### UI Layout & Flow (if applicable)
|
||||||
|
|
||||||
|
**Screen/Component**: Global Page Shell + Section Pages (Dashboards, Tasks, Reports)
|
||||||
|
|
||||||
|
* **Layout**:
|
||||||
|
* Unified vertical rhythm for all primary pages.
|
||||||
|
* Consistent top shell: breadcrumb/context (when present), page title, page subtitle/helper text, actions region.
|
||||||
|
* Content area uses standardized containers/cards with the same spacing and header/body semantics.
|
||||||
|
* **Key Elements**:
|
||||||
|
* **Primary Action Button**: Same visual hierarchy and placement rule in each section.
|
||||||
|
* **Secondary Actions**: Same grouping and alignment rules near the primary action.
|
||||||
|
* **Card Containers**: Same elevation/border/radius language and internal spacing.
|
||||||
|
* **State Blocks**: Loading, empty, success, and error states share one visual and textual pattern.
|
||||||
|
* **States**:
|
||||||
|
* **Default**: Structured and clean; key action is easy to locate in <5 seconds.
|
||||||
|
* **Loading**: Placeholder/skeleton or loader appears in a consistent zone without layout jumps.
|
||||||
|
* **Success**: Confirmation message style is consistent in tone and placement.
|
||||||
|
* **Error**: Error state uses consistent emphasis and an immediate recovery action (e.g., retry).
|
||||||
|
* **Empty**: Neutral empty message with guidance on next action.
|
||||||
|
|
||||||
|
## 4. The "Error" Experience
|
||||||
|
|
||||||
|
**Philosophy**: Errors should be consistent, informative, and recovery-oriented—never dead ends.
|
||||||
|
|
||||||
|
### Scenario A: Validation / Input Conflict
|
||||||
|
|
||||||
|
* **User Action**: User applies an invalid filter or submits an incomplete form.
|
||||||
|
* **System Response**:
|
||||||
|
* The problematic field/area is highlighted consistently across modules.
|
||||||
|
* A concise message explains what is wrong and what to do next.
|
||||||
|
* **Recovery**: User can correct input in-place and retry immediately, without page reload.
|
||||||
|
|
||||||
|
### Scenario B: Data Loading Failure
|
||||||
|
|
||||||
|
* **User Action**: User opens a page and backend request fails.
|
||||||
|
* **System Response**:
|
||||||
|
* A standardized error block appears in the content area.
|
||||||
|
* The message format is consistent with other modules.
|
||||||
|
* A clear recovery action is presented (e.g., "Retry").
|
||||||
|
* **Recovery**: User retries from the same state; on success the page returns to normal layout without disorientation.
|
||||||
|
|
||||||
|
## 5. Tone & Voice
|
||||||
|
|
||||||
|
* **Style**: Concise, clear, and confidence-building.
|
||||||
|
* **Terminology**: Use consistent terms across the product (e.g., one canonical term per concept), avoid mixed synonyms in state messages and actions.
|
||||||
@@ -30,6 +30,14 @@
|
|||||||
| `report_card.ux.test.js` | `lifecycle_function_unavailable` | Svelte 5 Vitest environment mismatch (mount on server error). Logic verified via integration tests. |
|
| `report_card.ux.test.js` | `lifecycle_function_unavailable` | Svelte 5 Vitest environment mismatch (mount on server error). Logic verified via integration tests. |
|
||||||
| `report_detail.ux.test.js` | `lifecycle_function_unavailable` | Same as above. |
|
| `report_detail.ux.test.js` | `lifecycle_function_unavailable` | Same as above. |
|
||||||
|
|
||||||
|
## Semantic Protocol Validation (2026-02-23)
|
||||||
|
|
||||||
|
- Ran semantic map generation via `python3 generate_semantic_map.py`.
|
||||||
|
- Latest compliance artifact: `semantics/reports/semantic_report_20260223_144408.md`.
|
||||||
|
- Global Semantic Compliance Score: **91.0%**.
|
||||||
|
- Global parser status: **0 errors / 0 warnings**.
|
||||||
|
- CRITICAL semantic issue for `backend/src/api/routes/__tests__/test_reports_detail_api.py` (missing `@INVARIANT`) has been resolved and file is now at **100%** in the latest report.
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [ ] Resolve Svelte 5 testing environment configuration for direct component mounting.
|
- [ ] Resolve Svelte 5 testing environment configuration for direct component mounting.
|
||||||
|
|||||||
Reference in New Issue
Block a user