# 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 (53 теста, все зелёные) - **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-`, не silent wipe - **Schema migrations**: `__schemaVersion` поле + `MIGRATIONS: Records>` 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 - **Dev-only**: `dev:simulateMatchEnd` gated на `!app.isPackaged` ### 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` | окно напоминания | ## Тесты (53) ``` src/shared/types.test.ts (4) src/shared/quiet-hours.test.ts (7) src/renderer/src/lib/format.test.ts (8) src/renderer/src/lib/history.test.ts (13) src/main/games/vdf.test.ts (11) src/renderer/src/i18n/i18n.test.ts (10) ``` Покрываются: helpers, история/стрики (DST), тихие часы (wrap+filter), VDF-парсер Steam, i18n с плюрализацией, дефолты. ## Технический долг (не для пользователя) - `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/негативы - Не амендить коммиты без явной просьбы пользователя