Files
laude/CLAUDE.md
AnRil bef733a877 feat(meals): вкладка «Питание» — напоминания о еде по времени суток
Новая модель Meal — напоминание по настенным часам (time HH:MM + дни недели),
в отличие от interval-based Exercise. Отдельная вкладка «Питание» с пресетами
быстрого добавления (Завтрак/Обед/Ужин/Перекус).

- shared: тип Meal, meals в AppState, nextMealOccurrence (DST-safe), SAMPLE_MEALS,
  MEAL_PRESETS; IPC-каналы meal:* + evtFireMeal
- main: валидация (строгая HH:MM-проверка диапазона), store-мутаторы с пересчётом
  nextFireAt, scheduler.checkDueMeals (гейт только globalEnabled, grace-окно 120с,
  игнор тихих часов/ВКС), notifications.fireMealReminder, IPC-хендлеры
- renderer: вкладка Meals + MealEditor (время/дни/иконка), MealReminder в окне
  напоминания (Поел/Отложить, TTS), пункт в Sidebar, маршрут, i18n RU/EN, иконки
  UtensilsCrossed/Soup
- persistence: meals additive (без bump схемы — старые state'ы получают [])
- +24 теста (203 -> 227): nextMealOccurrence, валидаторы приёмов пищи,
  scheduler meal-gating (вкл/выкл, grace, игнор тихих часов)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:45:34 +07:00

206 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (227 тестов, все зелёные)
- **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`
### Питание / приёмы пищи (вкладка «Питание»)
- **Отдельная модель `Meal`** (`src/shared/types.ts`): напоминание ПО ВРЕМЕНИ СУТОК
(`time` HH:MM + `days` weekdays), в отличие от interval-based `Exercise`
- `nextMealOccurrence(time, days, fromMs)` — следующее срабатывание, календарная
арифметика (DST-safe, как history.ts). Тесты в `src/shared/meals.test.ts`
- Scheduler `checkDueMeals()` (`src/main/scheduler.ts`): гейтит **только**
`globalEnabled` (НЕ тихие часы / НЕ ВКС — время задано пользователем явно).
Grace-окно `MEAL_GRACE_MS=120s`: приём, пропущенный давно (сон/выкл), тихо
переносится без срабатывания, чтобы не вывалить пачку напоминаний разом
- Окно напоминания: `evtFireMeal``MealReminder` в `ReminderApp.tsx` (зелёный
акцент `bg-success`, кнопки «Поел» / «Отложить»)
- Пресеты быстрого добавления — `MEAL_PRESETS` (имена через i18n-ключи)
- Время валидируется строго (`validHHMM` в validate.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://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude` (Punycode для `президент.рф`; Gitea переехал с `…/git/` на сабдомен `git.`)
- 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` | таймеры упражнений (interval) + приёмы пищи (clock-time), 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/pages/Meals.tsx` + `components/MealEditor.tsx` | вкладка «Питание» |
| `src/renderer/src/ReminderApp.tsx` | окно напоминания (упражнение / еда / матч) |
## Тесты (227)
```
src/main/validate.test.ts (78)
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/scheduler.test.ts (13) ← main: gating + приёмы пищи
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/shared/meals.test.ts (8) ← nextMealOccurrence (DST-safe)
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 (4)
```
Покрываются: IPC-валидация (упражнения/челленджи/приёмы пищи), persistence
(миграции/карантин/cap), scheduler-gating (тихие часы/ВКС/daily-goal), планирование
приёмов пищи по времени суток (DST-safe, grace-окно, игнор тихих часов), детект ВКС
(мок 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/негативы
- Не амендить коммиты без явной просьбы пользователя