Новый экранчик для обзора дашей

This commit is contained in:
2026-02-23 15:54:20 +03:00
parent 0685f50ae7
commit 43dd97ecbf
35 changed files with 32880 additions and 828 deletions

View File

@@ -2,12 +2,12 @@
> High-level module structure for AI Context. Generated automatically.
**Generated:** 2026-02-23T11:15:39.876570
**Generated:** 2026-02-23T14:44:08.540853
## Summary
- **Total Modules:** 71
- **Total Entities:** 1340
- **Total Modules:** 70
- **Total Entities:** 1337
## Module Hierarchy
@@ -116,9 +116,9 @@
### 📁 `__tests__/`
- 🏗️ **Layers:** API, Domain (Tests)
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 16, TRIVIAL: 21
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 16, TRIVIAL: 22
- 📄 **Files:** 5
- 📦 **Entities:** 40
- 📦 **Entities:** 41
**Key Entities:**
@@ -561,9 +561,9 @@
### 📁 `reports/`
- 🏗️ **Layers:** Domain
- 📊 **Tiers:** CRITICAL: 5, STANDARD: 13
- 📊 **Tiers:** CRITICAL: 5, STANDARD: 15
- 📄 **Files:** 3
- 📦 **Entities:** 18
- 📦 **Entities:** 20
**Key Entities:**
@@ -813,9 +813,9 @@
### 📁 `layout/`
- 🏗️ **Layers:** UI, Unknown
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 4, TRIVIAL: 24
- 📊 **Tiers:** CRITICAL: 3, STANDARD: 4, TRIVIAL: 26
- 📄 **Files:** 4
- 📦 **Entities:** 31
- 📦 **Entities:** 33
**Key Entities:**
@@ -851,9 +851,9 @@
### 📁 `reports/`
- 🏗️ **Layers:** UI, Unknown
- 📊 **Tiers:** CRITICAL: 4, STANDARD: 1, TRIVIAL: 9
- 📊 **Tiers:** CRITICAL: 4, STANDARD: 1, TRIVIAL: 10
- 📄 **Files:** 4
- 📦 **Entities:** 14
- 📦 **Entities:** 15
**Key Entities:**
@@ -1235,20 +1235,6 @@
- 📄 **Files:** 1
- 📦 **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/`
- 🏗️ **Layers:** UI

View File

@@ -115,6 +115,7 @@
- ⬅️ READS_FROM `lib`
- ⬅️ READS_FROM `t`
- ƒ **spawnTask** (`Function`)
- 📝 Execute task creation request and emit user feedback.
- 📦 **DashboardTypes** (`Module`) `[TRIVIAL]`
- 📝 TypeScript interfaces for Dashboard entities
- 🏗️ Layer: Domain
@@ -236,7 +237,9 @@
- 🏗️ Layer: Domain (Tests)
- 🔒 Invariant: Sidebar store transitions must be deterministic across desktop/mobile toggles.
- ƒ **test_sidebar_initial_state** (`Function`)
- 📝 Verify initial sidebar store values when no persisted state is available.
- ƒ **test_toggleSidebar** (`Function`)
- 📝 Verify desktop sidebar expansion toggles deterministically.
- ƒ **test_setActiveItem** (`Function`)
- ƒ **test_mobile_functions** (`Function`)
- 📦 **frontend.src.lib.stores.__tests__.test_activity** (`Module`)
@@ -332,6 +335,8 @@
- 🏗️ Layer: Unknown
- ƒ **getStatusClass** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **getStatusLabel** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **formatDate** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **onSelect** (`Function`) `[TRIVIAL]`
@@ -412,6 +417,8 @@
- 📦 **Sidebar** (`Module`) `[TRIVIAL]`
- 📝 Auto-generated module for frontend/src/lib/components/layout/Sidebar.svelte
- 🏗️ Layer: Unknown
- ƒ **buildCategories** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleItemClick** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleCategoryToggle** (`Function`) `[TRIVIAL]`
@@ -463,6 +470,8 @@
- 📝 Auto-detected function (orphan)
- ƒ **formatBreadcrumbLabel** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **getCrumbMeta** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- 🧩 **TaskDrawer** (`Component`) `[CRITICAL]`
- 📝 Global task drawer for monitoring background operations
- 🏗️ Layer: UI
@@ -481,7 +490,7 @@
- 🏗️ Layer: Unknown
- ƒ **handleClose** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **goToTasksPage** (`Function`) `[TRIVIAL]`
- ƒ **goToReportsPage** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **handleOverlayClick** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
@@ -512,29 +521,6 @@
- 📝 Bind global layout shell and conditional login/full-app rendering.
- 🏗️ Layer: UI
- 🔒 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]`
- 📝 Dataset Hub - Dedicated hub for datasets with mapping progress
- 🏗️ Layer: UI
@@ -2466,6 +2452,8 @@
- 📝 Auto-detected function (orphan)
- ƒ **test_get_reports_filter_and_pagination** (`Function`) `[TRIVIAL]`
- 📝 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]`
- 📝 Auto-detected function (orphan)
- 📦 **backend.src.api.routes.__tests__.test_datasets** (`Module`)
@@ -2473,6 +2461,7 @@
- 🏗️ Layer: API
- 🔒 Invariant: Endpoint contracts remain stable for success and validation failure paths.
- ƒ **test_get_datasets_success** (`Function`)
- 📝 Validate successful datasets listing contract for an existing environment.
- ƒ **test_get_datasets_env_not_found** (`Function`)
- ƒ **test_get_datasets_invalid_pagination** (`Function`)
- ƒ **test_map_columns_success** (`Function`)
@@ -2481,6 +2470,7 @@
- 📦 **backend.tests.test_reports_detail_api** (`Module`) `[CRITICAL]`
- 📝 Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
- 🏗️ Layer: Domain (Tests)
- 🔒 Invariant: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
- ƒ **__init__** (`Function`) `[TRIVIAL]`
- 📝 Auto-detected function (orphan)
- ƒ **get_all_tasks** (`Function`) `[TRIVIAL]`
@@ -2756,6 +2746,7 @@
- 🏗️ Layer: Service
- 🔒 Invariant: Resource summaries preserve task linkage and status projection behavior.
- ƒ **test_get_dashboards_with_status** (`Function`)
- 📝 Validate dashboard enrichment includes git/task status projections.
- ƒ **test_get_datasets_with_status** (`Function`)
- ƒ **test_get_activity_summary** (`Function`)
- ƒ **test_get_git_status_for_dashboard_no_repo** (`Function`)
@@ -2805,6 +2796,12 @@
- ƒ **_load_normalized_reports** (`Function`)
- 📝 Build normalized reports from all available tasks.
- 🔒 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`)
- 📝 Apply query filtering to a report.
- 🔒 Invariant: Filter evaluation is side-effect free.

View File

@@ -32,6 +32,7 @@ Use these for code generation (Style Transfer).
## 3. DOMAIN MAP (Modules)
* **Module Map:** `.ai/MODULE_MAP.md` -> `[DEF:Module_Map]`
* **Project Map:** `.ai/PROJECT_MAP.md` -> `[DEF:Project_Map]`
* **Apache Superset OpenAPI:** `.ai/openapi.json` -> `[DEF:Doc:Superset_OpenAPI]`
* **Backend Core:** `backend/src/core` -> `[DEF:Module:Backend_Core]`
* **Backend API:** `backend/src/api` -> `[DEF:Module:Backend_API]`
* **Frontend Lib:** `frontend/src/lib` -> `[DEF:Module:Frontend_Lib]`

30933
.ai/openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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)
- 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)
- 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)
@@ -63,9 +65,9 @@ cd src; pytest; ruff check .
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
## 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
- 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 -->

View File

@@ -146,6 +146,77 @@ def test_get_dashboards_invalid_pagination():
# [/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]
# @TEST: POST /api/dashboards/migrate creates migration task
# @PRE: Valid source_env_id, target_env_id, dashboard_ids

View File

@@ -4,6 +4,7 @@
# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
# @LAYER: Domain (Tests)
# @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 types import SimpleNamespace

View File

@@ -16,6 +16,7 @@ from typing import List, Optional, Dict
from pydantic import BaseModel, Field
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.superset_client import SupersetClient
# [/SECTION]
router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"])
@@ -52,6 +53,41 @@ class DashboardsResponse(BaseModel):
total_pages: int
# [/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]
# @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
@@ -132,6 +168,39 @@ async def get_dashboards(
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboards: {str(e)}")
# [/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]
class MigrateRequest(BaseModel):
source_env_id: str = Field(..., description="Source environment ID")

View File

@@ -11,6 +11,7 @@
# [SECTION: IMPORTS]
import json
import re
import zipfile
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union, cast
@@ -120,6 +121,252 @@ class SupersetClient:
return result
# [/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]
# @PURPOSE: Экспортирует дашборд в виде ZIP-архива.
# @PARAM: dashboard_id (int) - ID дашборда для экспорта.
@@ -246,6 +493,15 @@ class SupersetClient:
# @RELATION: CALLS -> self.network.request (for related_objects)
def get_dataset_detail(self, dataset_id: int) -> Dict:
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
response = self.get_dataset(dataset_id)
@@ -259,12 +515,15 @@ class SupersetClient:
columns = dataset.get("columns", [])
column_info = []
for col in columns:
col_id = col.get("id")
if col_id is None:
continue
column_info.append({
"id": col.get("id"),
"id": int(col_id),
"name": col.get("column_name"),
"type": col.get("type"),
"is_dttm": col.get("is_dttm", False),
"is_active": col.get("is_active", True),
"is_dttm": as_bool(col.get("is_dttm"), default=False),
"is_active": as_bool(col.get("is_active"), default=True),
"description": col.get("description", "")
})
@@ -286,11 +545,25 @@ class SupersetClient:
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({
"id": dash.get("id"),
"title": dash.get("dashboard_title") or dash.get("title", "Unknown"),
"id": int(dash_id),
"title": dash.get("dashboard_title") or dash.get("title", f"Dashboard {dash_id}"),
"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:
app_logger.warning(f"[get_dataset_detail][Warning] Failed to fetch related dashboards: {e}")
linked_dashboards = []
@@ -302,14 +575,18 @@ class SupersetClient:
"id": dataset.get("id"),
"table_name": dataset.get("table_name"),
"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", ""),
"columns": column_info,
"column_count": len(column_info),
"sql": sql,
"linked_dashboards": 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"),
"changed_on": dataset.get("changed_on")
}

View File

@@ -17,11 +17,11 @@ services:
timeout: 5s
retries: 10
app:
backend:
build:
context: .
dockerfile: Dockerfile
container_name: ss_tools_app
dockerfile: docker/backend.Dockerfile
container_name: ss_tools_backend
restart: unless-stopped
depends_on:
db:
@@ -33,11 +33,22 @@ services:
AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
BACKEND_PORT: 8000
ports:
- "8000:8000"
- "${BACKEND_HOST_PORT:-8001}:8000"
volumes:
- ./config.json:/app/config.json
- ./backups:/app/backups
- ./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:
postgres_data:

View File

@@ -1,16 +1,4 @@
# Stage 1: Build frontend static assets
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
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=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
COPY backend/ /app/backend/
COPY --from=frontend-build /app/frontend/build /app/frontend/build
WORKDIR /app/backend

View 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
View 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;
}
}

View File

@@ -187,6 +187,7 @@ export const api = {
if (options.page_size) params.append('page_size', options.page_size);
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}`),
// Datasets

View File

@@ -184,7 +184,7 @@
<!-- Sidebar -->
<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'}
{isMobileOpen
? 'translate-x-0 w-sidebar'
@@ -192,7 +192,7 @@
>
<!-- Header -->
<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-center'}"
>
@@ -214,7 +214,7 @@
<div>
<!-- Category Header -->
<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
? 'bg-primary-light text-primary md:border-r-2 md:border-primary'
: ''}"

View File

@@ -191,7 +191,7 @@
<!-- Drawer Overlay -->
{#if isOpen}
<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:keydown={(e) => e.key === 'Escape' && handleClose()}
role="button"
@@ -200,13 +200,13 @@
>
<!-- Drawer Panel -->
<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"
aria-modal="true"
aria-label="Task drawer"
>
<!-- 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">
{#if !activeTaskId && recentTasks.length > 0}
<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} />
</button>
{/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'}
</h2>
{#if shortTaskId}
@@ -235,7 +235,7 @@
</div>
<div class="flex items-center gap-2">
<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}
>
{$t.nav?.reports || "Reports"}
@@ -261,7 +261,7 @@
/>
{:else if loadingTasks}
<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>
</div>
{:else if recentTasks.length > 0}

View File

@@ -89,14 +89,14 @@
</script>
<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'}"
>
<!-- Left section: Hamburger (mobile) + Logo -->
<div class="flex items-center gap-2">
<!-- Hamburger Menu (mobile only) -->
<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}
aria-label="Toggle menu"
>
@@ -106,7 +106,7 @@
<!-- Logo/Brand -->
<a
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">
<Icon name="layers" size={18} strokeWidth={2.1} />
@@ -128,10 +128,10 @@
</div>
<!-- Nav Actions -->
<div class="flex items-center space-x-4">
<div class="flex items-center gap-3 md:gap-4">
<!-- Activity Indicator -->
<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:keydown={(e) =>
(e.key === "Enter" || e.key === " ") && handleActivityClick()}

View File

@@ -24,11 +24,19 @@
const profileLabel = $derived(typeof profile?.label === 'function' ? profile.label() : profile?.label);
function getStatusClass(status) {
if (status === 'success') return 'bg-green-100 text-green-700';
if (status === 'failed') return 'bg-red-100 text-red-700';
if (status === 'in_progress') return 'bg-blue-100 text-blue-700';
if (status === 'partial') return 'bg-amber-100 text-amber-700';
return 'bg-slate-100 text-slate-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 ring-1 ring-red-200';
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 ring-1 ring-amber-200';
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) {
@@ -44,7 +52,7 @@
</script>
<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}
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')}
</span>
<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>
</div>
<p class="text-sm font-medium text-slate-800">{report?.summary || ($t.reports?.not_provided || 'Not provided')}</p>

View File

@@ -31,13 +31,13 @@
}
</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>
{#if !detail || !detail.report}
<p class="text-sm text-slate-500">{$t.reports?.not_provided || 'Not provided'}</p>
{: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">Type:</span> {notProvided(detail.report.task_type)}</p>
<p><span class="text-slate-500">Status:</span> {notProvided(detail.report.status)}</p>
@@ -46,13 +46,13 @@
</div>
<div class="mt-4">
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">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>
<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-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>
{#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">
<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">
{#each (detail.next_actions && detail.next_actions.length > 0 ? detail.next_actions : detail.report.error_context.next_actions) as action}
<li>{action}</li>

View File

@@ -89,12 +89,8 @@
"storage_repo_pattern": "Repository Directory Pattern",
"storage_filename_pattern": "Filename Pattern",
"storage_preview": "Path Preview",
"environments": "Superset Environments",
"env_description": "Configure Superset environments for dashboards and datasets.",
"env_add": "Add Environment",
"env_actions": "Actions",
"env_test": "Test",
"env_delete": "Delete",
"connections_description": "Configure database connections for data mapping.",
"llm_description": "Configure LLM providers for dataset documentation.",
"logging": "Logging Configuration",
@@ -161,8 +157,6 @@
"action_migrate": "Migrate",
"action_backup": "Backup",
"action_commit": "Commit",
"git_status": "Git Status",
"last_task": "Last Task",
"view_task": "View task",
"task_running": "Running...",
"task_done": "Done",
@@ -170,23 +164,25 @@
"task_waiting": "Waiting",
"status_synced": "Synced",
"status_diff": "Diff",
"status_synced": "Synced",
"status_diff": "Diff",
"status_error": "Error",
"task_running": "Running...",
"task_done": "Done",
"task_failed": "Failed",
"task_waiting": "Waiting",
"view_task": "View task",
"empty": "No dashboards found"
},
"reports": {
"title": "Reports",
"empty": "No reports available.",
"filtered_empty": "No reports match your filters.",
"loading": "Loading reports...",
"retry_load": "Retry loading",
"clear_filters": "Clear filters",
"unknown_type": "Other / Unknown Type",
"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": {
"empty": "No datasets found",

View File

@@ -89,12 +89,8 @@
"storage_repo_pattern": "Шаблон директории репозиториев",
"storage_filename_pattern": "Шаблон имени файла",
"storage_preview": "Предпросмотр пути",
"environments": "Окружения Superset",
"env_description": "Настройка окружений Superset для дашбордов и датасетов.",
"env_add": "Добавить окружение",
"env_actions": "Действия",
"env_test": "Тест",
"env_delete": "Удалить",
"connections_description": "Настройка подключений к базам данных для маппинга.",
"llm_description": "Настройка LLM провайдеров для документирования датасетов.",
"logging": "Настройка логирования",
@@ -160,8 +156,6 @@
"action_migrate": "Мигрировать",
"action_backup": "Создать бэкап",
"action_commit": "Зафиксировать",
"git_status": "Статус Git",
"last_task": "Последняя задача",
"view_task": "Просмотреть задачу",
"task_running": "Выполняется...",
"task_done": "Готово",
@@ -169,23 +163,25 @@
"task_waiting": "Ожидание",
"status_synced": "Синхронизировано",
"status_diff": "Различия",
"status_synced": "Синхронизировано",
"status_diff": "Различия",
"status_error": "Ошибка",
"task_running": "Выполняется...",
"task_done": "Готово",
"task_failed": "Ошибка",
"task_waiting": "Ожидание",
"view_task": "Просмотреть задачу",
"empty": "Дашборды не найдены"
},
"reports": {
"title": "Отчеты",
"empty": "Отчеты отсутствуют.",
"filtered_empty": "Нет отчетов по выбранным фильтрам.",
"loading": "Загрузка отчетов...",
"retry_load": "Повторить загрузку",
"clear_filters": "Сбросить фильтры",
"unknown_type": "Прочее / Неизвестный тип",
"not_provided": "Не указано",
"view_details": "Подробнее"
"view_details": "Подробнее",
"diagnostics": "Диагностика",
"next_actions": "Следующие действия",
"status_success": "Успешно",
"status_failed": "Ошибка",
"status_in_progress": "В процессе",
"status_partial": "Частично"
},
"datasets": {
"empty": "Датасеты не найдены",

View File

@@ -41,7 +41,7 @@
<Toast />
<main class="bg-gray-50 min-h-screen">
<main class="min-h-screen bg-slate-50">
{#if isLoginPage}
<div class="p-4">
<slot />
@@ -61,7 +61,7 @@
</div>
<!-- Page content -->
<div class="p-4 flex-grow">
<div class="flex-grow px-4 pb-6 pt-2 md:px-6">
<slot />
</div>

View File

@@ -110,14 +110,14 @@
});
</script>
<div class="container mx-auto max-w-6xl p-4">
<div class="mx-auto w-full max-w-7xl space-y-4">
<PageHeader
title={$t.reports?.title || 'Reports'}
subtitle={() => 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">
<select
bind:value={taskType}
@@ -140,14 +140,14 @@
</select>
<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()}
>
{$t.common?.refresh || 'Refresh'}
</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}
>
{$t.reports?.clear_filters || 'Clear filters'}
@@ -156,24 +156,24 @@
</div>
{#if loading}
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
{$t.common?.loading || 'Loading...'}
<div class="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
{$t.reports?.loading || 'Loading reports...'}
</div>
{: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>
<button class="mt-2 rounded border border-red-300 px-3 py-1 text-sm" on:click={() => loadReports()}>
{$t.common?.retry || 'Retry'}
<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.reports?.retry_load || $t.common?.retry || 'Retry'}
</button>
</div>
{: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.'}
</div>
{: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>
<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'}
</button>
</div>

View File

@@ -187,14 +187,14 @@
}
</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 -->
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">
{$t.settings?.title || "Settings"}
</h1>
<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}
>
{$t.common?.refresh || "Refresh"}
@@ -218,12 +218,14 @@
<!-- Loading State -->
{#if isLoading}
<div class="bg-white rounded-lg p-6 border border-gray-200">
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
<div class="animate-pulse bg-gray-200 rounded h-8"></div>
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<div class="space-y-3">
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
<div class="h-8 animate-pulse rounded bg-slate-200"></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>
{:else if settings}
<!-- Tabs -->
@@ -271,7 +273,7 @@
</div>
<!-- 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"}
<!-- Environments Tab -->
<div class="text-lg font-medium mb-4">

File diff suppressed because it is too large Load Diff

View 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.

View 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.

View 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.

View 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.

View 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

View 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).

View 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

View 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 12
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

View 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-orientednever 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.

View File

@@ -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_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
- [ ] Resolve Svelte 5 testing environment configuration for direct component mounting.