Files
laude/CLAUDE.md

11 KiB
Raw Blame History

CLAUDE.md

Контекст проекта для Claude Code. Читается при старте каждой сессии.

TL;DR

Laude / Exercise Reminder — Windows desktop приложение на Electron 33, которое напоминает делать упражнения и опционально парсит статистику матчей Dota 2 (через GSI) в количество повторений. Текущая версия — 0.5.4. Один разработчик (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-<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 layersrc/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)

Команды

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/негативы
  • Не амендить коммиты без явной просьбы пользователя