- Средняя кнопка тайтлбара теперь toggle maximize/restore (была hide-to-tray, но иконка Square вводила в заблуждение — выглядит как нативная maximize). Double-click по тайтлбару тоже работает. - Иконка свапается Square ↔ Copy в зависимости от max-state, aria-label локализован (titlebar.maximize_aria / restore_aria). - Новый IPC: toggleMaximizeMain, isMaximizedMain (invoke), evtMaximizeChanged (event main → renderer на maximize/unmaximize). - Фикс drag-зоны: titlebar-nodrag перенесён с обёртки правого кластера на сами кнопки. Из-за flex-1 basis-0 пустое место слева от кнопок раньше было no-drag — окно нельзя было ухватить рядом. - minWidth/minHeight окна 900x600 → 1100x700, чтобы Tailwind lg: всегда срабатывал (4 hero-stat в один ряд, heatmap без скролла). - CLAUDE.md: контекст проекта для будущих сессий Claude Code (стек, архитектура, команды, релиз, тех. долг, чего не делать).
11 KiB
11 KiB
CLAUDE.md
Контекст проекта для Claude Code. Читается при старте каждой сессии.
TL;DR
Laude / Exercise Reminder — Windows desktop приложение на Electron 33, которое напоминает делать упражнения и опционально парсит статистику матчей Dota 2 (через GSI) в количество повторений. Текущая версия — 0.5.2. Один разработчик (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 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:simulateMatchEndgated на!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публикует одной командой в: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 |
окно напоминания |
Тесты (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/негативы
- Не амендить коммиты без явной просьбы пользователя