Надёжность 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>
12 KiB
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, наружу уходит genericipc-failed(не падаем молча, не утекают детали) - Dev-only:
dev:simulateMatchEndgated на!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публикует одной командой в:vX.Y.Z(постоянный архивный тег)update-channel(rolling — клиенты проверяют отсюда)- Опциональные
-BridgeTagsдля миграции старых пользователей
Безопасность
- GSI server (
src/main/games/gsi-server.ts): per-install token verify черезtimingSafeEqual, reject Origin/Sec-Fetch-Site (CSRF), 256KB body cap, requireapplication/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()(calendarsetDate, не 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)
Команды
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/негативы
- Не амендить коммиты без явной просьбы пользователя