From a95f070441fe7bcf46814dcc08ac088873a1b6ba Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 2 Jun 2026 09:55:39 +0300 Subject: [PATCH] feat(frontend): add DashboardHub components, EmptyState, and validation scaffold - DashboardHubModel.svelte.ts: Screen Model for dashboard listing hub with filters, pagination, and selection state. - DashboardRow.svelte: Individual dashboard card with Git/health badges. - ColumnFilterPopover.svelte: Column-based filter dropdown for dashboards. - dashboard-helpers.ts: Shared dashboard utility functions. - EmptyState.svelte: Reusable empty state component with icon and CTA. - validation route scaffold for per-dashboard validation views. --- frontend/ai-native-design-audit.md | 877 ++++++++++++++++++ frontend/scripts/audit-frontend-style.mjs | 224 +++++ .../lib/models/DashboardHubModel.svelte.ts | 586 ++++++++++++ frontend/src/lib/ui/EmptyState.svelte | 45 + .../dashboards/ColumnFilterPopover.svelte | 68 ++ .../src/routes/dashboards/DashboardRow.svelte | 193 ++++ 6 files changed, 1993 insertions(+) create mode 100644 frontend/ai-native-design-audit.md create mode 100644 frontend/scripts/audit-frontend-style.mjs create mode 100644 frontend/src/lib/models/DashboardHubModel.svelte.ts create mode 100644 frontend/src/lib/ui/EmptyState.svelte create mode 100644 frontend/src/routes/dashboards/ColumnFilterPopover.svelte create mode 100644 frontend/src/routes/dashboards/DashboardRow.svelte diff --git a/frontend/ai-native-design-audit.md b/frontend/ai-native-design-audit.md new file mode 100644 index 00000000..4117661b --- /dev/null +++ b/frontend/ai-native-design-audit.md @@ -0,0 +1,877 @@ +# Frontend Design Audit: ss-tools + +> Дата: 2026-06-01 +> Цель: Аудит фронтенд кода на предмет "плавающего" дизайна, выработка AI-native правил +> Объём: ~120 файлов, 8 UI-атомов, 55+ компонентов, 9 моделей, 9 сторов, все страницы, tailwind.config.js + +--- + +## Содержание + +1. [Методология](#1-методология) +2. [Архитектурный ландшафт](#2-архитектурный-ландшафт) +3. [Найденные проблемы](#3-найденные-проблемы) + - 3.1 [Критические](#31-критические) + - 3.2 [Средние](#32-средние) + - 3.3 [Малые](#33-малые) +4. [Причины плавающего дизайна](#4-причины-плавающего-дизайна) +5. [AI-Native Design Rules](#5-ai-native-design-rules) +6. [Приоритетный план исправлений](#6-приоритетный-план-исправлений) +7. [Приложение: Карта файлов](#7-приложение-карта-файлов) + +--- + +## 1. Методология + +### Сканированные области + +``` +frontend/ +├── tailwind.config.js ✅ конфиг темы +├── src/ +│ ├── app.css ✅ 3 строки (tailwind directives) +│ ├── routes/ ✅ все 35+ страниц +│ ├── lib/ +│ │ ├── ui/ ✅ 8 атомарных компонентов +│ │ ├── components/ ✅ 30+ доменных компонентов +│ │ ├── stores/ ✅ 9 сторов (8 legacy + 1 runes) +│ │ ├── models/ ✅ 9 моделей +│ │ ├── api/ ✅ клиентский слой +│ │ └── utils/ ✅ утилиты +│ └── components/ ✅ 30+ компонентов старого стиля +``` + +### Критерии оценки + +Каждый файл проверялся по 7 осям: +1. **Design tokens** — использует ли семантические цвета из `tailwind.config.js` или сырые классы +2. **Component reuse** — использует ли `$lib/ui` атомы или дублирует стили +3. **State architecture** — есть ли Screen Model для сложных страниц +4. **Svelte 5 compliance** — runes-only или legacy паттерны +5. **TypeScript** — типизирован или JSDoc/any +6. **UX contracts** — полнота семантических аннотаций +7. **Module size** — не превышает ли 400 строк (INV_7) + +--- + +## 2. Архитектурный ландшафт + +### 2.1 Tailwind Config (`tailwind.config.js`) + +Определены семантические цвета, которые должны быть единым источником истины: + +```js +theme: { + extend: { + colors: { + primary: { + DEFAULT: '#2563eb', // blue-600 + hover: '#1d4ed8', // blue-700 + ring: '#3b82f6', // blue-500 + light: '#eff6ff', // blue-50 + }, + secondary: { + DEFAULT: '#f3f4f6', // gray-100 + hover: '#e5e7eb', // gray-200 + text: '#111827', // gray-900 + ring: '#6b7280', // gray-500 + }, + destructive: { + DEFAULT: '#dc2626', // red-600 + hover: '#b91c1c', // red-700 + ring: '#ef4444', // red-500 + light: '#fef2f2', // red-50 + }, + ghost: { + hover: '#f3f4f6', // gray-100 + text: '#374151', // gray-700 + ring: '#6b7280', // gray-500 + }, + terminal: { /* dark palette for log viewer */ }, + log: { /* debug, info, warning, error */ }, + source: { /* plugin, api, git, system */ }, + }, + width: { + sidebar: '240px', + 'sidebar-collapsed': '64px', + }, + fontFamily: { + mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'], + }, + }, +} +``` + +### 2.2 UI-атомы (`src/lib/ui/`) + +| Компонент | Описание | Использует семантические токены | +|-----------|----------|----------------------------------| +| `Button.svelte` | primary/secondary/danger/ghost variants | ✅ `bg-primary`, `bg-destructive` | +| `Card.svelte` | Container with title, padding variants | ❌ `border-gray-200 bg-white` | +| `Input.svelte` | Text input with label + error | ❌ `border-gray-300`, `focus:ring-primary-ring` | +| `Select.svelte` | Dropdown with label | ❌ `border-gray-300` | +| `PageHeader.svelte` | Title + subtitle + actions slot | ❌ `text-gray-900` | +| `Icon.svelte` | SVG icon set | ❌ цвета не использует (currentColor) | +| `HelpTooltip.svelte` | Tooltip helper | ❌ | +| `LanguageSwitcher.svelte` | Lang toggle | ❌ | + +> **Вывод:** Только Button системно использует семантические токены. Остальные атомы используют сырые Tailwind-классы. Это источник "плавания" — если поменять primary цвет в конфиге, Button изменится, а Card/Input/Select — нет. + +### 2.3 Состояние: 3 параллельных подхода + +``` +┌────────────────────────────────────────────────────────────────┐ +│ STATE MANAGEMENT MAP │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ LEGACY STORES (svelte/store writable) │ +│ ├── sidebar.ts — sidebar state + localStorage │ +│ ├── taskDrawer.ts — task drawer visibility │ +│ ├── datasetReviewSession.ts — dataset review session │ +│ ├── environmentContext.ts — env context + selection │ +│ ├── health.ts — health check state │ +│ ├── activity.ts — derived activity │ +│ ├── assistantChat.ts — assistant chat session │ +│ └── translationRun.ts — translation run tracking │ +│ │ +│ MODERN STORES (.svelte.ts with $state) │ +│ ├── maintenance.svelte.ts — maintenance + WebSocket │ +│ │ +│ SCREEN MODELS (.svelte.ts class with $state) │ +│ ├── MigrationModel.svelte.ts — migration wizard state │ +│ ├── GitManagerModel.svelte.ts — git manager state │ +│ ├── GitStatusModel.svelte.ts — git status per dashboard │ +│ ├── GitConfigModel.svelte.ts — git configuration │ +│ ├── BranchModel.svelte.ts — branch management │ +│ ├── DeploymentModel.svelte.ts — deployment state │ +│ ├── CommitModel.svelte.ts — commit history state │ +│ ├── MigrationSettingsModel.svelte.ts — migration settings │ +│ └── MappingsModel.svelte.ts — db mapping state │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +**Проблема:** AI-агент видит 3 разных паттерна и не знает, какой выбрать для нового кода. + +### 2.4 Структура компонентов: двойная иерархия + +``` +src/lib/ui/ # Атомарные UI-компоненты (8 файлов) +├── Button.svelte +├── Card.svelte +├── Input.svelte +├── Select.svelte +├── PageHeader.svelte +├── Icon.svelte +├── HelpTooltip.svelte +└── LanguageSwitcher.svelte + +src/lib/components/ # Доменные компоненты (30+ файлов) +├── layout/ +├── translate/ +├── reports/ +├── dataset-review/ +├── ui/ # SearchableMultiSelect, MultiSelect +├── health/ +├── llm/ +└── assistant/ + +src/components/ # Старые компоненты (30+ файлов) +├── auth/ +├── git/ +├── tasks/ +├── tools/ +├── backups/ +├── storage/ +├── llm/ +├── EnvSelector.svelte +├── DashboardGrid.svelte +├── MappingTable.svelte +├── TaskRunner.svelte +├── TaskHistory.svelte +├── Toast.svelte +├── Navbar.svelte +├── Footer.svelte +├── StartupEnvironmentWizard.svelte +└── ... +``` + +**Проблема:** `src/components/` — наследие, но непонятно, почему часть компонентов там, а часть в `lib/components/`. Для AI-агента это создаёт неопределённость. + +### 2.5 Карта страниц и их состояния + +| Страница | Строк | $state атомов | Model? | Использует $lib/ui? | Типизация | +|----------|-------|---------------|--------|---------------------|-----------| +| `dashboards/+page.svelte` | **2765** | ~30+ | ❌ нет | ❌ частично | `lang="ts"` | +| `datasets/+page.svelte` | 472 | ~15+ | ❌ нет | ❌ нет | `lang="ts"` | +| `migration/+page.svelte` | 637 | ~5 (в модели) | ✅ MigrationModel | ✅ Button, Card, PageHeader | `lang="ts"` | +| `settings/+page.svelte` | 290 | ~5 | ❌ нет | ❌ нет | `lang="ts"` | +| `validation-tasks/+page.svelte` | 519 | ~10 | ❌ нет | ❌ нет | ❌ JSDoc | +| `reports/+page.svelte` | 204 | ~6 | ❌ нет | ✅ PageHeader | ❌ без типов | +| `dashboards/[id]/+page.svelte` | 582 | ~15 | частично (GitStatusModel) | ❌ нет | `lang="ts"` | +| `datasets/review/+page.svelte` | — | — | ❌ | — | — | +| `translate/+page.svelte` | — | — | ❌ | — | — | + +--- + +## 3. Найденные проблемы + +### 3.1 🔴 Критические + +#### 🔴 Проблема 1: Трёхголовая архитектура состояния + +**Описание:** В проекте одновременно существуют 3 несвязанных подхода к управлению состоянием. + +**Доказательство:** + +```typescript +// ПАТТЕРН 1: Legacy writable store (src/lib/stores/sidebar.ts) +import { writable, type Writable } from 'svelte/store'; +export const sidebarStore: Writable = writable(initialState); +// Используется через fromStore() в компонентах + +// ПАТТЕРН 2: Rune-based store (src/lib/stores/maintenance.svelte.ts) +export function createMaintenanceStore(): MaintenanceStore { + let activeEvents = $state([]); + // ... + return { get activeEvents() { return activeEvents; }, ... }; +} + +// ПАТТЕРН 3: Screen Model class (src/lib/models/MigrationModel.svelte.ts) +export class MigrationModel { + currentStep: number = $state(1); + // ... +} +``` + +**Последствия:** +- AI-агент не может определить, какой паттерн использовать +- `writable` сторы нельзя использовать с `$derived` напрямую +- Разный DX для тестирования: Model тестируется без DOM (`new Model()`), Store — через `get()` +- Рефакторинг любого стора требует контекстного переключения + +--- + +#### 🔴 Проблема 2: Нет Screen Model для сложных страниц + +**Описание:** Страницы с 10+ `$state` атомами и cross-widget инвариантами не выделены в Model. + +**Доказательство — `dashboards/+page.svelte` (2765 строк, фрагмент):** + +```svelte + + +``` + +**Сравнение с model-first подходом (migration/+page.svelte, 637 строк):** + +```svelte + + +``` + +**Последствия:** +- `dashboards/+page.svelte` невозможно протестировать unit-тестами без DOM +- AI-агент теряет контекст к 20-й функции +- Нарушение INV_7 (>400 строк, >10 cyclomatic complexity) +- Любые изменения рискуют сломать невидимые cross-widget инварианты + +--- + +#### 🔴 Проблема 3: Размытие дизайн-токенов + +**Описание:** Семантические цвета из `tailwind.config.js` используются непоследовательно. Рядом с `bg-primary` соседствуют `bg-indigo-50`, `bg-gradient-to-br from-slate-50 via-white to-sky-50`, `bg-red-100`. + +**Доказательство — сравнение паттернов в dashboards/+page.svelte:** + +```svelte + + + + + {/if} +
+ {#each (showSearch ? options : staticOptions) as value} + + {/each} +
+ + \ No newline at end of file diff --git a/frontend/src/routes/dashboards/DashboardRow.svelte b/frontend/src/routes/dashboards/DashboardRow.svelte new file mode 100644 index 00000000..6f6dff9a --- /dev/null +++ b/frontend/src/routes/dashboards/DashboardRow.svelte @@ -0,0 +1,193 @@ + + + + + +
+ +
+ +
+ + +
+ +
+ + +
+
+ + {dashboard.git?.hasRepo === null ? $t.common?.loading || "Loading" : dashboard.git?.hasRepo ? $t.dashboard?.status_repo || "Repo" : $t.dashboard?.status_no_repo || "No Repo"} + + {#if dashboard.git?.hasRepo} + + {dashboard.git?.hasChangesForCommit ? $t.dashboard?.status_changes || "Diff" : $t.dashboard?.status_no_changes || "Synced"} + + {/if} +
+
+ + +
+ {#if validationStatus} + {@const vs = validationStatus} + {@const dotClass = vs.status === "PASS" ? "bg-emerald-400" : vs.status === "WARN" ? "bg-amber-400" : vs.status === "FAIL" ? "bg-rose-400" : "bg-slate-300"} + + {:else if validationStatusLoading} + + + + {:else} + - + {/if} + + + {#if isValidationPopoverOpen} +
e.stopPropagation()} + > +
+ {$t.dashboard?.validation_history || "Validation History"} +
+ {#if validationStatus?.history?.length > 0} +
+ {#each validationStatus.history.slice(0, 3) as run} +
+
+ + {run.status || "UNKNOWN"} +
+
+ {run.task_name || "-"} + {formatDate(run.last_run_at)} +
+
+ {/each} +
+ {:else} +
+ {$t.dashboard?.no_validation_runs || "No validation runs yet"} +
+ {/if} + +
+ {/if} +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+ {dashboard.changedOnLabel} +
+ + +
+ {#if dashboard.owners?.length > 0} +
+ {#each dashboard.owners as owner (owner)} + + {owner} + + {/each} +
+ {:else} +
-
+ {/if} +
+
+ \ No newline at end of file