884 lines
38 KiB
Markdown
884 lines
38 KiB
Markdown
# Frontend Design Audit: ss-tools
|
||
|
||
> Дата: 2026-06-02 (ревизия 2)
|
||
> Цель: Аудит фронтенд кода на предмет "плавающего" дизайна, выработка AI-native правил
|
||
> Объём: ~120 файлов, 8 UI-атомов, 55+ компонентов, 9 моделей, 9 сторов, все страницы, tailwind.config.js + ROUTES registry + link-integrity test
|
||
|
||
---
|
||
|
||
## Содержание
|
||
|
||
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-статус-исправлений)
|
||
8. [Следующие шаги для AI-First](#8-следующие-шаги-для-ai-first)
|
||
9. [Приложение: Карта файлов](#9-приложение-карта-файлов)
|
||
|
||
---
|
||
|
||
## 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<SidebarState> = writable(initialState);
|
||
|
||
// ПАТТЕРН 2: Rune-based store (src/lib/stores/maintenance.svelte.ts)
|
||
export function createMaintenanceStore(): MaintenanceStore {
|
||
let activeEvents = $state<unknown[]>([]);
|
||
return { get activeEvents() { return activeEvents; }, ... };
|
||
}
|
||
|
||
// ПАТТЕРН 3: Screen Model class (src/lib/models/MigrationModel.svelte.ts)
|
||
export class MigrationModel {
|
||
currentStep: number = $state(1);
|
||
}
|
||
```
|
||
|
||
**Последствия:**
|
||
- AI-агент не может определить, какой паттерн использовать
|
||
- Разный DX для тестирования: Model тестируется без DOM, Store — через `get()`
|
||
|
||
---
|
||
|
||
#### 🔴 Проблема 2: Нет Screen Model для сложных страниц
|
||
|
||
**Описание:** Страницы с 10+ `$state` атомами и cross-widget инвариантами не выделены в Model.
|
||
|
||
**Доказательство — `dashboards/+page.svelte` (2765 строк):**
|
||
|
||
```svelte
|
||
<!-- ❌ ВСЁ В ОДНОМ ФАЙЛЕ: ~30 атомов, ~50 функций -->
|
||
<script lang="ts">
|
||
let allDashboards = $state([]);
|
||
let dashboards = $state([]);
|
||
let isLoading = $state(true);
|
||
let error = $state(null);
|
||
let currentPage = $state(1);
|
||
let searchQuery = $state("");
|
||
// ... ещё 20+ атомов, 50+ функций, Git-операции, всё инлайн
|
||
</script>
|
||
```
|
||
|
||
**Последствия:**
|
||
- Невозможно протестировать unit-тестами без DOM
|
||
- AI-агент теряет контекст к 20-й функции
|
||
- Нарушение INV_7 (>400 строк)
|
||
|
||
---
|
||
|
||
#### 🔴 Проблема 3: Мёртвые ссылки — навигация на несуществующие роуты
|
||
|
||
**Описание:** Ссылки формировались как сырые строки в 40+ местах. Если роут переименован или не создан — 404 в рантайме.
|
||
|
||
**Найденные битые роуты:**
|
||
|
||
| Ссылка | Откуда | Статус |
|
||
|--------|--------|--------|
|
||
| `/dashboards/{id}/validation?env_id=` | `dashboards/[id]/+page.svelte`, `dashboards/health/+page.svelte` | ❌ → ✅ СОЗДАН |
|
||
| `/validation/{taskId}` | `ScheduleAtAGlance.svelte` | ❌ → ✅ ИСПРАВЛЕН |
|
||
|
||
**Исправление (2026-06-02):**
|
||
- Создан `$lib/routes.ts` — централизованный реестр с type-safe builder-функциями
|
||
- 47 raw goto/href мигрированы на `ROUTES.*()` (21 файл)
|
||
- Добавлен link-integrity тест (46 тестов + сканирование raw-строк)
|
||
- Создан роут `/dashboards/{id}/validation/`
|
||
- Починена ссылка `/validation/{taskId}` → `/validation-tasks/{taskId}`
|
||
|
||
```typescript
|
||
// Было (размазано): goto(`/dashboards/${id}/validation?env_id=${envId}`);
|
||
// Стало (SSOT): goto(ROUTES.dashboards.validation(id, envId));
|
||
```
|
||
|
||
**Результат:** 698 тестов, 0 raw goto/href, 0 битых роутов.
|
||
|
||
**Новые AI-Native правила (Rules 9-10, см. раздел 5.2):**
|
||
- **Rule 9:** Все навигационные ссылки — только через `ROUTES.*()`
|
||
- **Rule 10:** Link-integrity тест обязателен в CI
|
||
|
||
---
|
||
|
||
#### 🔴 Проблема 4: Размытие дизайн-токенов
|
||
|
||
**Описание:** Семантические цвета из `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
|
||
<!-- ✅ Семантический токен (редко) -->
|
||
<button class="bg-primary text-white hover:bg-primary-hover">
|
||
|
||
<!-- ❌ Дрейф: indigo вместо primary -->
|
||
<button class="inline-flex items-center justify-center rounded-lg border border-indigo-300 bg-indigo-50 px-4 py-2 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-100">
|
||
|
||
<!-- ❌ Дрейф: сырой red вместо destructive -->
|
||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between">
|
||
|
||
<!-- ❌ Рядом destructive (правильно) и сырой red (неправильно) -->
|
||
<button class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors">
|
||
|
||
<!-- ❌ sky-50, gradient — нет в токенах -->
|
||
<div class="relative overflow-hidden rounded-3xl border border-slate-200 bg-gradient-to-br from-slate-50 via-white to-sky-50 p-8 shadow-sm">
|
||
```
|
||
|
||
**Примеры из других файлов:**
|
||
|
||
```svelte
|
||
<!-- validation-tasks/+page.svelte — сырые цвета -->
|
||
<select class="rounded-md border border-slate-300 px-2 py-1.5 text-sm">
|
||
|
||
<!-- settings/+page.svelte — сырые цвета -->
|
||
<div class="border-b border-gray-200">
|
||
|
||
<!-- reports/+page.svelte — сырой Card вместо компонента Card -->
|
||
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||
|
||
<!-- settings/+page.svelte — 3 разных стиля кнопок в одном файле -->
|
||
<button class="bg-blue-600 text-white px-3 py-1.5 text-sm rounded hover:bg-blue-700">
|
||
<button class="px-3 py-1.5 text-sm border border-gray-300 rounded hover:bg-gray-100">
|
||
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||
```
|
||
|
||
**Последствия:**
|
||
- Изменение `primary` в tailwind.config.js не синхронизируется с indigo/sky-кнопками
|
||
- AI-агент не может определить "правильный" цвет — каждый раз гадает
|
||
- Визуальная несогласованность: кнопки на одной странице выглядят по-разному
|
||
- 5 разных стилей для одного и того же UI-паттерна
|
||
|
||
---
|
||
|
||
### 3.2 🟡 Средние
|
||
|
||
#### 🟡 Проблема 5: Button-компонент не стандартизирован
|
||
|
||
**Описание:** Только `migration/+page.svelte` системно использует `$lib/ui/Button`. 7 разных стилей ручных `<button>` в проекте.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 6: Дублирование каталогов компонентов
|
||
|
||
**Описание:** 3 корневые директории (`lib/ui/`, `lib/components/`, `components/`) без чётких правил. `src/components/` — замороженное наследие.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 7: 33 `any` в TypeScript
|
||
|
||
**Описание:** 33 вхождения `any` в исходниках — функции `getRunLink(run: any)`, `dashboard: any`, `catch (err: any)`.
|
||
**Ключевые цели:**
|
||
- `function getRunLink(run: any)` → конкретный интерфейс `Run`
|
||
- `dashboard: any` → `Dashboard`
|
||
- `catch (err: any)` → `catch (err: unknown)`
|
||
|
||
**Последствие:** AI-агент вынужден угадывать форму данных. `any` — главный источник галлюцинаций.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 8: JSDoc вместо TypeScript
|
||
|
||
**Описание:** `validation-tasks/+page.svelte` и другие страницы используют JSDoc вместо `<script lang="ts">`.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 9: 8 легаси-сторов блокируют миграцию на runes
|
||
|
||
**Описание:** Все сторы, кроме `maintenance.svelte.ts`, используют Svelte 4 `writable()`.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 10: Неравномерное покрытие UX-контрактами
|
||
|
||
`@UX_REACTIVITY` отсутствует везде, кроме MigrationPage. См. таблицу ниже.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 11: Нет Component Catalog — SSOT для UI-компонентов
|
||
|
||
**Описание:** AI-агент не может за 1 чтение узнать, какие компоненты существуют и какие у них пропсы.
|
||
**Масштаб:** 8 атомов + 30+ доменных компонентов — все с разными интерфейсами пропсов.
|
||
|
||
---
|
||
|
||
#### 🟡 Проблема 12: Нет API Data Contracts
|
||
|
||
**Описание:** `api.ts` содержит 30+ endpoint-методов, но ни один не документирует входные/выходные DTO.
|
||
AI-агент не знает форму API-ответов без чтения бэкенда.
|
||
|
||
---
|
||
|
||
### 3.3 🟢 Малые
|
||
|
||
#### 🟢 Проблема 13: `cn()` utility не строго типизирован
|
||
|
||
---
|
||
|
||
#### 🟢 Проблема 14: Дублирование debounce
|
||
|
||
---
|
||
|
||
#### 🟢 Проблема 15: Zero-state не переиспользуется
|
||
|
||
---
|
||
|
||
#### 🟢 Проблема 16: Border radius плавает
|
||
|
||
---
|
||
|
||
## 4. Причины плавающего дизайна
|
||
|
||
### 4.1 Отсутствие единого Design Language
|
||
|
||
У проекта есть `tailwind.config.js` с семантическими токенами, но нет документированных правил:
|
||
- Когда использовать `primary` vs `blue-600` vs `indigo-600`?
|
||
- Какой border radius для чего?
|
||
- Какой набор компонентов обязателен к использованию?
|
||
|
||
### 4.2 Две эпохи Svelte
|
||
|
||
Проект начинался на Svelte 4 (writable stores, on:event, export let), потом мигрировал на Svelte 5 (runes, $state). Миграция не завершена:
|
||
- 8 stores остались на writable
|
||
- 30+ компонентов в `src/components/` — наследие
|
||
- Новые страницы пишутся на runes, но без model-first
|
||
|
||
### 4.3 AI-агенты без правил
|
||
|
||
Каждый AI-агент (человек или LLM) при создании новой страницы:
|
||
1. Не знает, какой цвет использовать — выбирает "похожий"
|
||
2. Не знает, какой компонент переиспользовать — пишет с нуля
|
||
3. Не знает, куда положить файл — кладёт в `routes/` или `components/`
|
||
4. Не знает паттерн состояния — пишет `$state` + функции инлайн
|
||
|
||
### 4.4 Нет "шлюза" для нового кода
|
||
|
||
Нет code review checklist или pre-commit хука, который проверял бы:
|
||
- Использует ли страница `<Button>` вместо ручного `<button>`?
|
||
- Использует ли страница семантические токены?
|
||
- Есть ли Screen Model для страницы с >5 атомами?
|
||
|
||
---
|
||
|
||
## 5. AI-Native Design Rules
|
||
|
||
> Эти правила предназначены для включения в промпт AI-агента (Svelte-кодера) и для CI-валидации.
|
||
|
||
### Rule 1: Semantic Color Tokens Over Raw Colors
|
||
|
||
```svelte
|
||
// ✅ CORRECT
|
||
<div class="bg-primary text-white hover:bg-primary-hover">
|
||
<div class="bg-destructive text-white">
|
||
<div class="bg-secondary text-secondary-text">
|
||
|
||
// ❌ WRONG
|
||
<div class="bg-blue-600 text-white">
|
||
<div class="bg-indigo-50 text-indigo-700">
|
||
<div class="bg-red-100 text-red-700">
|
||
<div class="bg-gradient-to-br from-slate-50 via-white to-sky-50">
|
||
```
|
||
|
||
**Исключение:** Только в `tailwind.config.js` при определении новых токенов.
|
||
|
||
---
|
||
|
||
### Rule 2: UI Atoms Are Mandatory For Pages
|
||
|
||
```svelte
|
||
// ✅ CORRECT
|
||
import { Button, Card, Input, Select, PageHeader } from "$lib/ui";
|
||
|
||
<Button variant="primary" onclick={handleSave}>
|
||
<Card>
|
||
<PageHeader title="Settings" subtitle={() => <p>Sub</p>} />
|
||
|
||
// ❌ WRONG
|
||
<button class="px-3 py-1 bg-primary rounded text-white">
|
||
<div class="bg-white rounded-xl border shadow-sm p-6">
|
||
<div class="flex items-center justify-between mb-8">
|
||
<h1 class="text-3xl font-bold">Settings</h1>
|
||
```
|
||
|
||
**Исключение:** layout-компоненты и сами атомы в `$lib/ui/`.
|
||
|
||
---
|
||
|
||
### Rule 3: Screen Model For Any Page With >5 $state Atoms
|
||
|
||
```typescript
|
||
// ✅ CORRECT: DashboardHubModel.svelte.ts
|
||
export class DashboardHubModel {
|
||
dashboards: Dashboard[] = $state([]);
|
||
page: number = $state(1);
|
||
searchQuery: string = $state("");
|
||
// ...
|
||
async loadDashboards(): Promise<void> { ... }
|
||
async deleteDashboard(id: string): Promise<void> { ... }
|
||
}
|
||
|
||
// ✅ CORRECT: migration/+page.svelte (thin render layer)
|
||
const model = new MigrationModel();
|
||
onMount(() => model.loadEnvironments());
|
||
|
||
// ❌ WRONG: dashboards/+page.svelte (2765 строк, ~30 $state атомов инлайн)
|
||
```
|
||
|
||
**Trigger:** Если в `<script>` страницы больше 5 `$state(...)` вызовов → создай Model.
|
||
|
||
---
|
||
|
||
### Rule 4: Runes Only For New State Code
|
||
|
||
```typescript
|
||
// ✅ CORRECT (.svelte.ts)
|
||
let count = $state(0);
|
||
const doubled = $derived(count * 2);
|
||
|
||
// ❌ WRONG (.ts)
|
||
import { writable, derived } from 'svelte/store';
|
||
const count = writable(0);
|
||
const doubled = derived(count, $count => $count * 2);
|
||
```
|
||
|
||
**Исключение:** Только рефакторинг существующих writable-сторов, у которых есть подписчики.
|
||
|
||
---
|
||
|
||
### Rule 5: One Component Directory — `lib/components/`
|
||
|
||
```
|
||
// ✅ CORRECT
|
||
src/lib/components/git/GitManager.svelte
|
||
src/lib/components/git/BranchSelector.svelte
|
||
|
||
// ❌ WRONG
|
||
src/components/git/GitManager.svelte
|
||
src/components/git/BranchSelector.svelte
|
||
```
|
||
|
||
**Trigger:** Новый доменный компонент → `src/lib/components/<domain>/`. `src/components/` — заморожено.
|
||
|
||
---
|
||
|
||
### Rule 6: TypeScript Required On Pages
|
||
|
||
```svelte
|
||
// ✅ CORRECT
|
||
<script lang="ts">
|
||
let { data }: { data: PageData } = $props();
|
||
let items: Item[] = $state([]);
|
||
|
||
// ❌ WRONG
|
||
<script>
|
||
/** @type {Array<any>} */ let items = $state([]);
|
||
```
|
||
|
||
---
|
||
|
||
### Rule 7: Consistent Border Radius Hierarchy
|
||
|
||
```
|
||
rounded-md → inputs, buttons, selects (h-10)
|
||
rounded-lg → cards, containers, modals
|
||
rounded-xl → panels, drawers (опционально)
|
||
rounded-3xl → zero-state / empty-state только
|
||
```
|
||
|
||
---
|
||
|
||
### Rule 8: Page Max 400 Lines
|
||
|
||
```
|
||
>400 lines → Model + Component decomposition
|
||
>10 cyclomatic complexity → helper functions
|
||
```
|
||
|
||
---
|
||
|
||
### Rule 9: Route Registry For Navigation (NEW 2026-06-02)
|
||
|
||
```svelte
|
||
// ✅ CORRECT
|
||
import { ROUTES } from '$lib/routes.js';
|
||
<a href={ROUTES.dashboards.validation(id, envId)}>History</a>
|
||
goto(ROUTES.dashboards.detail(id, envId));
|
||
|
||
// ❌ WRONG — raw string, неверифицируемо, риск 404
|
||
<a href={`/dashboards/${id}/validation?env_id=${envId}`}>History</a>
|
||
goto(`/dashboards/${id}?env_id=${envId}`);
|
||
```
|
||
|
||
**Trigger:** Любая навигация (`goto`, `href`, `window.location.href`) → только через `ROUTES.*()`.
|
||
|
||
**Исключение:** Внешние URL (`/api/auth/login/adfs`).
|
||
|
||
---
|
||
|
||
### Rule 10: Link Integrity Test Required (NEW 2026-06-02)
|
||
|
||
```
|
||
Перед каждым CI-run:
|
||
1. vitest run src/lib/__tests__/routes-link-integrity.test.ts
|
||
2. Падать при любых raw goto/href строках (кроме ROUTES)
|
||
3. Падать, если ROUTES builder не匹配 ни один файловый роут
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Приоритетный план исправлений
|
||
|
||
### ✅ Выполнено (2026-06-02)
|
||
|
||
| # | Задача | Статус |
|
||
|---|--------|--------|
|
||
| ✅ | Создать `ROUTES` registry (`$lib/routes.ts`) | ✅ Все 46 builder-функций |
|
||
| ✅ | Link-integrity тест | ✅ 698 тестов проходят |
|
||
| ✅ | Мигрировать 47 raw goto/href → ROUTES | ✅ 21 файл изменён |
|
||
| ✅ | Создать роут `/dashboards/{id}/validation/` | ✅ Было 404 → отдаёт данные |
|
||
| ✅ | Починить `/validation/{taskId}` → `/validation-tasks/{taskId}` | ✅ Было 404 → работает |
|
||
|
||
### P0 — Немедленно (2-3 часа)
|
||
|
||
| # | Задача | Файлы | Ожидаемый результат |
|
||
|---|--------|-------|---------------------|
|
||
| 0.1 | Выделить `DashboardHubModel.svelte.ts` | `dashboards/+page.svelte` | 2765 → <400 строк |
|
||
| 0.2 | Создать `EmptyState` компонент | `lib/ui/EmptyState.svelte` | Единый zero-state |
|
||
|
||
### P1 — Важно (5-6 часов)
|
||
|
||
| # | Задача | Файлы | Ожидаемый результат |
|
||
|---|--------|-------|---------------------|
|
||
| 1.1 | **Component Catalog** — SSOT всех UI-компонентов с пропсами | `$lib/components.ts` | AI-агент видит все компоненты за 1 чтение |
|
||
| 1.2 | **API Data Contracts** — `@DATA_CONTRACT` на все endpoint-методы | `api.ts` | AI-агент знает DTO без чтения бэкенда |
|
||
| 1.3 | Типизация `any` (33 вхождения) | 15+ файлов | `run: any` → `Run`, `dashboard: any` → `Dashboard` |
|
||
|
||
### P2 — Среднесрочно (4-5 часов)
|
||
|
||
| # | Задача | Файлы | Ожидаемый результат |
|
||
|---|--------|-------|---------------------|
|
||
| 2.1 | Заменить raw colors на semantic tokens | Все page-файлы | Ни одного `bg-indigo-*` в page |
|
||
| 2.2 | Заменить ручные `<button>` на `<Button>` | Все page-файлы | Единый стиль кнопок |
|
||
| 2.3 | Мигрировать 1-2 легаси стора на `.svelte.ts` | `taskDrawer.ts`, `sidebar.ts` | Runes-only stores |
|
||
| 2.4 | Выделить `DatasetHubModel` | `datasets/+page.svelte` | model-first для datasets |
|
||
| 2.5 | Выделить `ValidationTasksModel` | `validation-tasks/+page.svelte` | model-first для validation |
|
||
|
||
### P3 — Долгосрочно (6-8 часов)
|
||
|
||
| # | Задача | Файлы | Ожидаемый результат |
|
||
|---|--------|-------|---------------------|
|
||
| 3.1 | Консолидировать `src/components/` в `lib/components/` | 30+ файлов | Одна иерархия |
|
||
| 3.2 | Мигрировать оставшиеся 6 writable-сторов | 6 store-файлов | Runes-only |
|
||
| 3.3 | Семантические токены для всех UI-атомов | `Card`, `Input`, `Select`, `PageHeader` | Единые токены |
|
||
| 3.4 | Store Registry — SSOT для всех сторов | `$lib/stores/index.ts` | 1 файл, 30 строк |
|
||
|
||
---
|
||
|
||
## 7. Статус исправлений (2026-06-02)
|
||
|
||
### Что сделано
|
||
|
||
| Изменение | Файлы | Тесты |
|
||
|-----------|-------|-------|
|
||
| Создан `$lib/routes.ts` — ROUTES registry | 1 новый файл | 46 тестов на валидность каждого builder |
|
||
| Создан link-integrity тест | 1 новый файл | Сканирует все `src/` на raw goto/href |
|
||
| Создан роут `/dashboards/{id}/validation/` | 2 файла (+page.svelte, +page.ts) | Работает + пагинация + статус-бейджи |
|
||
| Мигрированы 47 raw goto/href → ROUTES | 21 файл изменён | 0 raw строк осталось |
|
||
| Исправлен битый роут `/validation/{id}` | 1 файл (ScheduleAtAGlance) | Теперь ведёт на `/validation-tasks/{id}` |
|
||
| Исправлены 2 контрактных теста | 2 тестовых файла | Проверяют `ROUTES.*()` вместо raw строк |
|
||
|
||
### Метрики качества
|
||
|
||
| Метрика | До | После |
|
||
|---------|:--:|:-----:|
|
||
| Raw goto/href строк в коде | 47 | **0** |
|
||
| Битых роутов | 2 | **0** |
|
||
| Тестов на целостность навигации | 0 | **46** |
|
||
| Файлов, изменённых для миграции | — | 21 |
|
||
| Всего тестов | 696 | **698** (все проходят) |
|
||
|
||
---
|
||
|
||
## 8. Следующие шаги для AI-First
|
||
|
||
На основе аудита и выполненной работы, следующие улучшения для AI-First (в порядке приоритета):
|
||
|
||
### 8.1 Component Catalog (`$lib/components.ts`)
|
||
|
||
**Почему:** AI-агент тратит ~5-10 чтений на поиск подходящего компонента. Component catalog сокращает это до 1.
|
||
|
||
**Формат:**
|
||
```typescript
|
||
export const COMPONENTS = {
|
||
Button: {
|
||
path: "$lib/ui/Button.svelte",
|
||
props: {} as ButtonProps,
|
||
variants: ["primary", "secondary", "destructive", "ghost"] as const,
|
||
},
|
||
Card: { path: "$lib/ui/Card.svelte", props: {} as CardProps },
|
||
PageHeader: { path: "$lib/ui/PageHeader.svelte", props: {} as PageHeaderProps },
|
||
// ... все 30+ компонентов
|
||
} as const;
|
||
```
|
||
|
||
**Трудоёмкость:** 1 файл, ~50 строк.
|
||
|
||
### 8.2 API Data Contracts
|
||
|
||
**Почему:** Каждый `fetchApi` в `api.ts` — чёрный ящик для AI-агента. Без DTO агент галлюцинирует поля.
|
||
|
||
**Формат:**
|
||
```typescript
|
||
// @BRIEF Fetch paginated dashboards for an environment
|
||
// @DATA_CONTRACT params -> { env_id, page, page_size, search?, filters? }
|
||
// @DATA_CONTRACT response -> { dashboards: Dashboard[], total: number, page: number }
|
||
getDashboards: <T = DashboardListResponse>(envId: string, opts?: DashboardQueryOptions) => ...
|
||
```
|
||
|
||
**Трудоёмкость:** 30+ методов, ~2-3 строки документации на каждый.
|
||
|
||
### 8.3 Типизация `any` (33 вхождения)
|
||
|
||
**Почему:** `any` — главный источник AI-галлюцинаций. Агент вынужден угадывать структуру.
|
||
|
||
**Ключевые цели:**
|
||
- `function getRunLink(run: any)` → конкретный интерфейс `Run`
|
||
- `dashboard: any` → `Dashboard`
|
||
- `catch (err: any)` → `catch (err: unknown)`
|
||
|
||
**Трудоёмкость:** ~15-20 файлов, механическая работа.
|
||
|
||
### 8.4 Store Registry (`$lib/stores/index.ts`)
|
||
|
||
**Почему:** Агент видит все глобальные состояния в одном месте.
|
||
|
||
**Трудоёмкость:** 1 файл, ~30 строк.
|
||
|
||
### 8.5 Semantic Index Maintenance
|
||
|
||
- Перестроить индекс с DuckDB для fuzzy-search
|
||
- Исправить 103 unresolved relations (информационный шум)
|
||
|
||
**Трудоёмкость:** 1 команда + фикс таргетов @RELATION.
|
||
|
||
---
|
||
|
||
## 9. Приложение: Карта файлов
|
||
|
||
### 7.1 UI-атомы (`src/lib/ui/`)
|
||
|
||
| Файл | Строк | Использует токены | Типы |
|
||
|------|-------|-------------------|------|
|
||
| `Button.svelte` | 75 | ✅ `bg-primary`, `bg-destructive` | `lang="ts"` |
|
||
| `Card.svelte` | 58 | ❌ `border-gray-200 bg-white` | `lang="ts"` |
|
||
| `Input.svelte` | 66 | ❌ `border-gray-300` + `focus:ring-primary-ring` | `lang="ts"` |
|
||
| `Select.svelte` | 58 | ❌ `border-gray-300` | `lang="ts"` |
|
||
| `PageHeader.svelte` | 45 | ❌ `text-gray-900` | `lang="ts"` |
|
||
| `Icon.svelte` | 111 | ✅ `currentColor` | `lang="ts"` |
|
||
| `HelpTooltip.svelte` | — | — | — |
|
||
| `LanguageSwitcher.svelte` | — | — | — |
|
||
|
||
### 7.2 Модели (`src/lib/models/`)
|
||
|
||
| Файл | Строк | Семантика |
|
||
|------|-------|-----------|
|
||
| `MigrationModel.svelte.ts` | 472 | ✅ model-first — gold standard |
|
||
| `GitManagerModel.svelte.ts` | — | ✅ |
|
||
| `GitStatusModel.svelte.ts` | — | ✅ |
|
||
| `GitConfigModel.svelte.ts` | — | ✅ |
|
||
| `BranchModel.svelte.ts` | — | ✅ |
|
||
| `DeploymentModel.svelte.ts` | — | ✅ |
|
||
| `CommitModel.svelte.ts` | — | ✅ |
|
||
| `MigrationSettingsModel.svelte.ts` | — | ✅ |
|
||
| `MappingsModel.svelte.ts` | — | ✅ |
|
||
|
||
### 7.3 Сторы (`src/lib/stores/`)
|
||
|
||
| Файл | Строк | Паттерн | Семантические токены |
|
||
|------|-------|---------|---------------------|
|
||
| `sidebar.ts` | 79 | `writable` ❌ legacy | ✅ аннотации |
|
||
| `taskDrawer.ts` | 112 | `writable` ❌ legacy | ✅ |
|
||
| `environmentContext.ts` | — | `writable` ❌ legacy | ✅ |
|
||
| `datasetReviewSession.ts` | 87 | `writable` ❌ legacy | ✅ |
|
||
| `assistantChat.ts` | — | `writable` ❌ legacy | ✅ |
|
||
| `health.ts` | — | `writable` ❌ legacy | ✅ |
|
||
| `activity.ts` | — | `writable` ❌ legacy | ✅ |
|
||
| `translationRun.ts` | — | `writable` ❌ legacy | ❌ |
|
||
| `maintenance.svelte.ts` | 203 | `$state` ✅ modern | ✅ |
|
||
|
||
### 7.4 Страницы (`src/routes/`)
|
||
|
||
| Файл | Строк | Model? | $lib/ui? | Типы | UX контракты |
|
||
|------|-------|--------|----------|------|-------------|
|
||
| `dashboards/+page.svelte` | 2765 | ❌ | ❌ частично | ts | ✅ 5/5 |
|
||
| `dashboards/[id]/+page.svelte` | 582 | частично | ❌ | ts | ✅ 4/5 |
|
||
| `datasets/+page.svelte` | 472 | ❌ | ❌ | ts | ❌ 1/5 |
|
||
| `datasets/review/+page.svelte` | — | ❌ | — | — | — |
|
||
| `migration/+page.svelte` | 637 | ✅ | ✅ | ts | ✅ 5/5 |
|
||
| `settings/+page.svelte` | 290 | ❌ | ❌ | ts | ✅ 4/5 |
|
||
| `validation-tasks/+page.svelte` | 519 | ❌ | ❌ | ❌ JSDoc | ✅ 4/5 |
|
||
| `reports/+page.svelte` | 204 | ❌ | ✅ PageHeader | ❌ any | ✅ 3/5 |
|
||
| `translate/+page.svelte` | — | ❌ | — | — | — |
|
||
| `+layout.svelte` | 112 | — | — | ts | ✅ |
|
||
|
||
### 7.5 Старые компоненты (`src/components/`)
|
||
|
||
```
|
||
src/components/
|
||
├── auth/
|
||
│ └── ProtectedRoute.svelte
|
||
├── git/
|
||
│ ├── GitManager.svelte
|
||
│ ├── BranchSelector.svelte
|
||
│ ├── CommitHistory.svelte
|
||
│ ├── CommitModal.svelte
|
||
│ ├── ConflictResolver.svelte
|
||
│ ├── DeploymentModal.svelte
|
||
│ ├── GitInitPanel.svelte
|
||
│ ├── GitOperationsPanel.svelte
|
||
│ ├── GitReleasePanel.svelte
|
||
│ └── GitWorkspacePanel.svelte
|
||
├── tasks/
|
||
│ ├── TaskResultPanel.svelte
|
||
│ └── TaskLogPanel.svelte
|
||
│ └── LogFilterBar.svelte
|
||
│ └── LogEntryRow.svelte
|
||
├── tools/
|
||
│ ├── MapperTool.svelte
|
||
│ └── DebugTool.svelte
|
||
├── backups/
|
||
│ ├── BackupManager.svelte
|
||
│ └── BackupList.svelte
|
||
├── storage/
|
||
│ ├── FileUpload.svelte
|
||
│ └── FileList.svelte
|
||
├── llm/
|
||
│ ├── ValidationReport.svelte
|
||
│ ├── ProviderConfig.svelte
|
||
│ └── DocPreview.svelte
|
||
├── DashboardGrid.svelte
|
||
├── EnvSelector.svelte
|
||
├── Footer.svelte
|
||
├── MappingTable.svelte
|
||
├── MissingMappingModal.svelte
|
||
├── Navbar.svelte
|
||
├── PasswordPrompt.svelte
|
||
├── RepositoryDashboardGrid.svelte
|
||
├── StartupEnvironmentWizard.svelte
|
||
├── TaskHistory.svelte
|
||
├── TaskList.svelte
|
||
├── TaskLogViewer.svelte
|
||
├── TaskRunner.svelte
|
||
├── Toast.svelte
|
||
├── DynamicForm.svelte
|
||
└── RepositoryDashboardGrid.svelte
|
||
```
|
||
|
||
---
|
||
|
||
*Аудит выполнен 2026-06-01. Полный контекст: ~120 файлов, 8 UI-атомов, 55+ компонентов, 9 моделей, 9 сторов, все 35+ страниц.*
|