Bug — Heatmap/streak/achievements не обновлялись после markDone/
markChallengeDone. Регресс из Sprint C (история выделена из
state-broadcast). Корень: store мутирует Exercise.lastDoneAt
in-place → state.exercises ref не меняется → useEffect([exercises])
не fires → Dashboard не перетягивает history.
Фикс: новый event IPC.evtHistoryChanged + broadcastHistoryChanged().
Триггерится после markDone/snooze/skip/markChallengeDone/
clearHistory/import. Dashboard.useEffect подписывается через
onHistoryChanged.
Settings → AboutCard теперь показывает текущую версию приложения
(раньше была только кнопка «Что нового»). Версия через
IPC.getAppVersion.
Tests:
+6 для repsDoneTodayForExercise — match-challenges, snapshot,
deleted-exercise fallback, ignore skip/snooze.
+2 для dailyReps с новыми snapshot-полями (match-challenges
и deleted exercises).
+6 для unseenVersions + RELEASE_NOTES контракт.
+7 для adjustNextFireAt (адаптивный шедулер): малая история,
плохой/хороший час, MAX_SHIFT_HOURS, фильтр по упражнению,
30-day window.
Итого 135 → 159 (+24).
Грепнул src/ на стейл-references к removed setPaused/isPaused/
`let paused` — чисто. Sprint C-D refactor завершён без residue.
#9 AppState больше не содержит `history` (вынесено в PersistedState
— internal store-shape). `broadcastState()` и IPC.getState шлют
через IPC только exercises/settings/challenges/gamesEnabled.
Раньше каждый markDone/snooze/toggle вызывал broadcastState() →
весь state, включая до 10k history-записей (~500KB JSON), летел
через IPC к каждому BrowserWindow и парсился в renderer'е. На
долгом горизонте использования становилось заметным лагом UI.
Renderer и раньше историю из state не читал (Dashboard вызывает
IPC.getHistory отдельно), так что это чистый perf-win без
behavioural change. Store-internal mutations продолжают работать
с полным PersistedState через `getState()`; renderer-bound IPC
использует новый `getStateForRenderer()`.
Не сделано из спринта C: zustand setState refactor (#8) — текущая
архитектура работает корректно (zustand bathes), `?? []` fallback'и
в селекторах безопасны. Реальный gain был от #9, который и закрыт.