P0 #1 — Match-челленджи теперь пишутся в историю.
HistoryEntry расширен полями `reps?`, `name?`, `source?` (snapshot
planned-reps + name на момент записи + 'reminder'/'match').
Новый store.markChallengeDone(challengeId, reps) пишет entry с
exerciseId='challenge:<id>' и source='match'.
Зарегистрирован IPC.markChallengeDone handler (раньше канал был в
enum, но handler не подключен).
ReminderApp.MatchSummaryView вызывает window.api.markChallengeDone
при ✓-клике. Стрик, today_done, achievements теперь учитывают
игровые тренировки.
Заодно dailyReps/dailyRepsRange/totalDoneReps используют
entry.reps как fallback — heatmap не теряет данные после удаления
упражнения (закрывает P2 #12).
P0 #2 — Tray-пауза синхронизирована с Dashboard.
Раньше tray держал scheduler-local `paused` boolean, который не
отражался в settings.globalEnabled — Dashboard показывал «running»
с тикающим таймером, хотя fires не приходили. Сейчас оба пути
(tray и Dashboard-кнопка) меняют единственный source of truth —
settings.globalEnabled. setPaused/isPaused/paused удалены, IPC
pauseAll/resumeAll переписаны на updateSettings.
P0 #3 — Whats-new покажется существующим пользователям при апгрейде.
Раньше для всех undefined lastSeenVersion (включая обновляющихся
с v0.5.5) делали silent-save без модалки — никто бы не увидел
v0.5.6 changelog. Сейчас: если есть Exercise с lastDoneAt → это
обновляющийся пользователь, показываем заметки текущей версии;
если нет — новичок, silent.
Second pass through the audit punch-list. ESLint and Prettier now clean
(0 errors, 0 warnings), typecheck clean, 53 tests pass.
ACCESSIBILITY (Modal)
- Full focus trap: Tab/Shift-Tab cycle within the dialog and never
escape to the underlying page.
- Focus restoration: closing returns focus to the trigger button.
- First focusable child is focused on open (skipping the X button).
- aria-labelledby links the dialog to its <h2> via useId().
- Close button's hardcoded "Закрыть" replaced with i18n key.
ERROR RECOVERY
- Add ErrorBoundary component (class — only way) with localized
fallback and a "try again" reset button. Stack trace shown only in
dev. Wrapped around the whole App + a nested boundary around the
routed pages so a crash in one route doesn't blank the chrome.
- Module-level guard on subscribeToBackend so React 18 StrictMode's
dev-mode double-mount doesn't subscribe twice.
- Loading placeholder is now blank (was hardcoded Russian "Загрузка…"
that English users would see during initial hydration).
TRAY i18n
- 5 tray strings now follow the current settings.language. Falls back
to Russian when the store isn't loaded yet or the lang is unknown.
- refreshMenu() called on settings.language change and on
pauseAll/resumeAll so the pause label stays in sync with state.
IPC VALIDATION (src/main/validate.ts)
- Hand-rolled validators for every renderer-supplied payload:
exercise input/patch, challenge input/patch, settings patch, id,
actualReps, snoozeMinutes. Range-check numeric fields
(intervalMinutes ∈ [1, 1440], reps ∈ [1, 9999], multiplier ∈ [0,
1000], snooze ∈ [1, 1440]), cap string lengths at 200, restrict
enums (theme/lang/notify-mode/stat) to known values, validate
quietHours.from/to with HH:MM regex and dedup quietHours.days.
- Every ipcMain.handle for mutations now runs the validator first and
returns null on rejection instead of pushing junk into the store.
A compromised renderer can no longer corrupt persisted state via
out-of-range numbers or wrong-type fields.
SCHEMA MIGRATIONS (src/main/store.ts)
- Add __schemaVersion field to persisted state with CURRENT = 1.
- MIGRATIONS map: { 0: (s) => s } as a no-op seed; future structural
changes (e.g. quietHours shape revision) get a single explicit slot.
- runMigrations() applies migrations in order; coerce() normalises the
result into a fully-formed AppState. Both first-write and every
flush() persist the version field.
EXHAUSTIVE-DEPS WARNINGS
- Dashboard: memoise `exercises` so downstream useMemos don't fire on
every parent render; gate the history fetch on exercises change
instead of any state change.
- HistoryHeatmap: wrap `weeks` in useMemo so monthLabels' deps are
stable.
LINT POLISH
- updater.ts: refactor a Prettier-vs-no-extra-semi conflict by
extracting the cast into a local binding.
- Remove dead import of `Challenge` from ipc.ts (now imported via
validators).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>