Надёжность main-процесса: - глобальные uncaughtException/unhandledRejection (лог + flushNow) - safeHandle/safeOn вокруг всех IPC-хендлеров (не падаем молча, generic-ошибка наружу) - таймаут 4s на tasklist, Atomics.wait вместо busy-spin на exit-записи - единый log.error для фоновых сбоев вместо console.error/тишины Тесты (178 -> 203): meeting-detect, scheduler-gating, store (миграции/карантин/cap). UI/UX: - prefers-reduced-motion через MotionConfig + CSS media-блок - Spinner/Skeleton примитивы, loading-состояния вместо пустых заглушек - aria-live анонсы достижений и выполнения (useAnnounce) - оформленные пустые состояния, клавиатура в меню ExerciseCard Лицензия: проприетарный LICENSE + правка README/CLAUDE.md, счётчик тестов. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
12 KiB
Markdown
189 lines
12 KiB
Markdown
# CLAUDE.md
|
||
|
||
Контекст проекта для Claude Code. Читается при старте каждой сессии.
|
||
|
||
## TL;DR
|
||
|
||
**Laude / Exercise Reminder** — Windows desktop приложение на Electron 33, которое напоминает делать упражнения и опционально парсит статистику матчей Dota 2 (через GSI) в количество повторений. Текущая версия — **0.5.8**. Один разработчик (AnRil), один remote — self-hosted Gitea.
|
||
|
||
## Стек
|
||
|
||
- **Runtime**: Electron 33 (main + preload + renderer)
|
||
- **Build**: electron-vite 2 + Vite 5 + electron-builder 25 (NSIS, x64 only)
|
||
- **UI**: React 18 + TypeScript 5 + Tailwind 3 + framer-motion + react-router (HashRouter) + zustand 5
|
||
- **Auto-update**: electron-updater 6, generic provider, фиксированный канал
|
||
- **Тесты**: Vitest 4 (203 теста, все зелёные)
|
||
- **Lint/format**: ESLint 8 (flat-ish .eslintrc.cjs) + Prettier 3 + EditorConfig
|
||
- **Иконки**: lucide-react (whitelisted lookup через `ICON_CHOICES`)
|
||
- **Шрифты**: Plus Jakarta Sans, Bricolage Grotesque, JetBrains Mono (Google Fonts CDN)
|
||
|
||
## Архитектура (важное)
|
||
|
||
### Процессы
|
||
- **main** (`src/main/`) — Node, scheduler, GSI HTTP-сервер, IPC, окна, tray, updater, persistence
|
||
- **preload** (`src/preload/index.ts`) — contextBridge → `window.api`, dev-only методы вырезаны на проде (`import.meta.env.MODE !== 'production'`)
|
||
- **renderer** (`src/renderer/src/`) — React, zustand-store, страницы Dashboard/Games/Settings/About, ReminderApp в отдельном окне
|
||
|
||
### Persistence
|
||
- Единственный JSON-файл: `%APPDATA%\Exercise Reminder\app-state.json`
|
||
- **Атомарная запись**: tmp + rename + retry на EBUSY/EPERM (антивирус, OneDrive)
|
||
- **Не теряет данные**: corrupt JSON → quarantine в `app-state.json.corrupt-<ts>`, не silent wipe
|
||
- **Schema migrations**: `__schemaVersion` поле + `MIGRATIONS: Record<number, (s)=>s>` map в `src/main/store.ts`
|
||
- **Debounced writes**: pendingWrite с `.unref()`
|
||
|
||
### IPC
|
||
- Типизированные каналы — `src/shared/ipc.ts`
|
||
- **Validation layer** — `src/main/validate.ts` (hand-rolled, без zod):
|
||
- `intervalMinutes ∈ [1, 1440]`, `reps ∈ [1, 9999]`, `multiplier ∈ [0, 1000]`
|
||
- string cap 200 chars, enum-валидация для theme/lang/notify-mode/stat
|
||
- HH:MM regex для quietHours, dedup days
|
||
- Strip `id` из updateExercise/updateChallenge patch
|
||
- **Error-boundary**: все хендлеры обёрнуты в `safeHandle`/`safeOn` (`src/main/ipc.ts`) — исключение логируется в `latest.log`, наружу уходит generic `ipc-failed` (не падаем молча, не утекают детали)
|
||
- **Dev-only**: `dev:simulateMatchEnd` gated на `!app.isPackaged`
|
||
|
||
### Отказоустойчивость main
|
||
- **Глобальные хендлеры** в `src/main/index.ts`: `uncaughtException` (лог + `flushNow`) и `unhandledRejection` (лог) — процесс не исчезает молча
|
||
- **tasklist timeout** 4s в `meeting-detect.ts` (зависший child не копится)
|
||
- **Sync write на exit** через `Atomics.wait` (не busy-spin) в `store.ts`
|
||
|
||
### Auto-update (КРИТИЧНО)
|
||
- **Фиксированный URL канала**: `…/releases/download/update-channel/latest.yml` — никогда не меняется
|
||
- **НЕ** `…/releases/download/v${version}/…` (старая схема ломалась: установленная копия видела только свой релиз)
|
||
- Hourly silent auto-check (транзитные сетевые ошибки не показывают красный баннер; только ручной клик показывает ошибку)
|
||
- Boot-check: 3 ретрая с backoff 30s/2m/5m
|
||
- `lastCheckedAt` → UI «проверено N мин назад»
|
||
- Релиз через `scripts/release.ps1` публикует одной командой в:
|
||
1. `vX.Y.Z` (постоянный архивный тег)
|
||
2. `update-channel` (rolling — клиенты проверяют отсюда)
|
||
3. Опциональные `-BridgeTags` для миграции старых пользователей
|
||
|
||
### Безопасность
|
||
- **GSI server** (`src/main/games/gsi-server.ts`): per-install token verify через `timingSafeEqual`, reject Origin/Sec-Fetch-Site (CSRF), 256KB body cap, require `application/json`, generic 400
|
||
- **shell.openExternal allowlist**: только `http:`/`https:`/`mailto:` (`src/main/windows.ts`)
|
||
- **will-navigate** блокирует non-file:// и non-dev URL
|
||
- **Modal focus trap** + focus restore, aria-labelledby
|
||
|
||
### Quiet hours
|
||
- `isQuietAt(time, settings)` в `src/shared/types.ts`
|
||
- Wrap-around (22:00 → 07:00) корректно — при wrap-active проверяется день *начала* окна
|
||
- Тесты в `src/shared/quiet-hours.test.ts`
|
||
|
||
### История / стрики
|
||
- `src/renderer/src/lib/history.ts` — DST-safe через `shiftDays()` (calendar `setDate`, не ms-арифметика)
|
||
- Cap 10k записей, trim oldest 10% на overflow
|
||
- HistoryHeatmap: percentile-based bucketing (p25/p50/p85), а не flat ratio (защищает от outlier-дней)
|
||
|
||
### i18n
|
||
- Самописная микро-система: `src/renderer/src/i18n/dict.ts` (плоский словарь ~200 ключей × 2 языка)
|
||
- Хук `useT()`, плюрализация CLDR rules для RU (one/few/many)
|
||
- Интерполяция через split/join (не regex — защита от regex-инъекций в значениях var)
|
||
- Tray menu тоже локализован (`TRAY_STRINGS` в `src/main/tray.ts`)
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
npm run dev # electron-vite dev
|
||
npm run typecheck # tsc по node + web
|
||
npm run test:run # vitest один раз
|
||
npm run lint # eslint --max-warnings 0
|
||
npm run format # prettier --write
|
||
npm run dist # сборка + NSIS installer → release/
|
||
|
||
# Релиз (всё в одном)
|
||
npm run release -- -Bump patch
|
||
# или -Bump minor / -Bump major / -Version 1.2.3
|
||
# опционально: -BridgeTags v0.4.0,v0.4.1
|
||
```
|
||
|
||
## Скрипты релиза
|
||
|
||
- `scripts/release.ps1` — bump → typecheck → test → build → tag → push → upload в Gitea (vX.Y.Z + update-channel + bridges)
|
||
- `scripts/upload-release-assets.ps1` — curl.exe с retry/backoff (15s/45s/2m/5m × 4) на 504/TLS, проверяет уже-залилось через list assets перед ретраем
|
||
- **PowerShell 5.1 gotchas**:
|
||
- Default reads CP1251 → файлы скриптов **ASCII-only**, без em-dash/кириллицы в коде
|
||
- `Set-Content -Encoding utf8` добавляет BOM → ломает PostCSS. Для UTF-8 без BOM использовать `[System.IO.File]::WriteAllText` + `new UTF8Encoding($false)`
|
||
- Никогда `-i` флаги (rebase -i, add -i) — нет interactive input
|
||
|
||
## Gitea remote
|
||
|
||
- URL: `https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude` (Punycode для `президент.рф`)
|
||
- User: `anril`
|
||
- Auth: см. `~/.claude/projects/.../memory/gitea_remote.md`
|
||
- **Actions выключены** (`has_actions: false`) — релизим через PowerShell, runners не настроены
|
||
- `.gitea/workflows/` пустая (раньше там лежали yml → queued runs копились)
|
||
|
||
## Файлы, которые часто правлю
|
||
|
||
| Файл | Что |
|
||
|---|---|
|
||
| `package.json` | version, publish.url, scripts, deps |
|
||
| `src/main/store.ts` | persistence, migrations, validation, atomic writes |
|
||
| `src/main/ipc.ts` | IPC handlers с валидацией |
|
||
| `src/main/scheduler.ts` | таймеры упражнений, powerMonitor |
|
||
| `src/main/games/dota2.ts` + `gsi-server.ts` | GSI приём матчей |
|
||
| `src/main/updater.ts` | auto-update logic, silent retries |
|
||
| `src/shared/types.ts` | shared типы, дефолты, isQuietAt |
|
||
| `src/shared/ipc.ts` | IPC channel types |
|
||
| `src/renderer/src/i18n/dict.ts` | словари |
|
||
| `src/renderer/src/pages/Dashboard.tsx` | главная |
|
||
| `src/renderer/src/ReminderApp.tsx` | окно напоминания |
|
||
|
||
## Тесты (203)
|
||
|
||
```
|
||
src/main/validate.test.ts (68)
|
||
src/renderer/src/lib/history.test.ts (31)
|
||
src/renderer/src/i18n/i18n.test.ts (15)
|
||
src/renderer/src/lib/format.test.ts (14)
|
||
src/main/games/vdf.test.ts (11)
|
||
src/main/store.test.ts (10) ← main: миграции/карантин/cap
|
||
src/renderer/src/lib/achievements.test.ts (10)
|
||
src/shared/release-notes.test.ts (9)
|
||
src/main/scheduler.test.ts (8) ← main: gating-логика
|
||
src/main/meeting-detect.test.ts (7) ← main: детект ВКС + кэш/timeout
|
||
src/shared/quiet-hours.test.ts (7)
|
||
src/main/adaptive.test.ts (6)
|
||
src/shared/types.test.ts (4)
|
||
src/renderer/src/lib/icon-choices.test.ts (3)
|
||
```
|
||
|
||
Покрываются: IPC-валидация, persistence (миграции/карантин/cap), scheduler-gating
|
||
(тихие часы/ВКС/daily-goal), детект ВКС (мок child_process), helpers, история/стрики
|
||
(DST), тихие часы (wrap+filter), VDF-парсер Steam, достижения, i18n с плюрализацией,
|
||
дефолты.
|
||
|
||
Паттерн для main-тестов: `vi.mock('electron'|'./store'|'node:child_process')` +
|
||
`vi.resetModules()` + dynamic import (сброс module-level состояния между тестами).
|
||
|
||
## Технический долг (не для пользователя)
|
||
|
||
- `sandbox: true` на BrowserWindow — нужен smoke-тест preload в sandbox-режиме
|
||
- Self-host Google Fonts (сейчас внешняя CSP-зависимость)
|
||
- ReminderApp race: первое напоминание может прийти без озвучки до загрузки settings
|
||
- Мажорные апдейты (React 18→19, Electron 33→42, Tailwind 3→4) — каждый ломающий, отдельная итерация
|
||
- Code-signing NSIS — ~$300/год, уберёт SmartScreen warning
|
||
- Скриншоты в README (есть TODO в самом README)
|
||
|
||
## Стиль кода
|
||
|
||
- Prettier: semi:false, singleQuote, trailingComma:none, printWidth:80
|
||
- ESLint: eslint:recommended + ts + react + react-hooks (без style rules — это Prettier)
|
||
- TypeScript strict, никакого `any` в новом коде
|
||
- Комментарии на русском там, где объясняют **почему**, не **что**
|
||
- Коммиты на русском, формат `тип(scope): кратко` (feat/fix/docs/refactor/test/chore)
|
||
- Co-Authored-By футер в коммитах от Claude
|
||
|
||
## Управление контекстом
|
||
|
||
- **Ужимать контекст при достижении 250k токенов** — вызывать `/compact` (или эквивалент) когда суммарный контекст подходит к 250 000 токенов. Не дожидаться authentic переполнения и автоматического сжатия от рантайма — сделать это контролируемо, чтобы важный контекст (открытые правки, недокоммиченные решения, текущая ветка задачи) попал в summary, а не выпал.
|
||
|
||
## Чего НЕ делать
|
||
|
||
- Не пушить в `update-channel` руками — только через `release.ps1`
|
||
- Не добавлять `.gitea/workflows/*.yml` — has_actions выключен, runs зависнут
|
||
- Не использовать regex в i18n-интерполяции — только split/join
|
||
- Не silent wipe corrupt JSON — quarantine с timestamp
|
||
- Не возвращать ms-арифметику в history.ts — DST сломается
|
||
- Не убирать validation layer из IPC — compromised renderer может засунуть NaN/негативы
|
||
- Не амендить коммиты без явной просьбы пользователя
|