Список «Питания» разбивался на два <Card> (активные/выключенные). При
переключении строка переезжала между списками → её Switch размонтировался и
монтировался заново, и ползунок анимировался только при включении (mount
x:0→20), а при выключении — нет.
Теперь единый keyed-список (включённые сверху): Switch остаётся смонтированным,
ползунок плавно ездит в обе стороны, а строка «переезжает» в свою группу через
framer layout-анимацию. Иконке добавлен transition-colors. Глобальный Switch не
трогал — Упражнения/Дашборд не затронуты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gitea переехал с path-prefix (xn--90adajar8af4h.xn--p1ai/git/) на
выделенный сабдомен (git.xn--90adajar8af4h.xn--p1ai). Старый URL теперь
отдаёт чужое приложение и для git мёртв.
- package.json: publish.url (канал авто-апдейта) -> новый хост
- scripts/release.ps1, upload-release-assets.ps1: $giteaHost (API + release URL)
- README, CHANGELOG, RELEASING.md, CLAUDE.md: ссылки на репозиторий/релизы
Прим.: уже установленные копии (<=0.5.8) запекли старый URL в бинарник —
их авто-апдейт нужно мигрировать отдельно (bridge-теги), правкой конфига
это ретроактивно не лечится.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReminderApp MatchSummary: sentChallengesRef для дедупа rapid double-click
на ✓ — раньше один и тот же challenge мог записаться в историю несколько
раз, давая лишние reps. Ref сбрасывается на новый match.
ExerciseCard «Готово» (для due-упражнения): такая же ref-based дедуп.
Окно ~1 сек между click → IPC.markDone → store.markDone обновляет
nextFireAt → broadcastState → ticks broadcast → isDue=false. До этого
быстрый double-click писал 2 entries с близкими ts.
ipc.ts: title в showSaveDialog/showOpenDialog локализован по
settings.language. Раньше всегда был русский в EN-локали.
Challenges editor: EMPTY_DRAFT.exerciseName: '' вместо 'Приседания'.
В EN-локали дефолтный русский текст выглядел багом. Required-валидация
не пускает пустое значение в save.
Bug: при отмене save-dialog или open-dialog DataCard показывал тост
«Не удалось сохранить» / «Файл не подошёл». Но cancel — это не ошибка.
Расширил IPC возврат: { ok, canceled, path?, error? }. UI теперь
различает: ok → success toast, !ok && canceled → молча, !ok && !canceled
→ error toast.
+9 тестов на validateSettingsPatch для voicePromptsEnabled,
meetingAutoPause, lastSeenVersion (semver-regex / null-сброс /
malformed). Итого 159 → 168 тестов.
Settings → About теперь показывает текущую версию приложения
(раньше была только кнопка «Что нового»). Загружается через
IPC.getAppVersion при mount.
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.
P2 #9 — Achievement unlock celebration. AchievementsCard сравнивает
текущие unlocked с persisted set'ом в localStorage. Новые играют
scale+accent-glow анимацию 2.8 сек (1.4с × 2 повтора). После
добавляются в celebrated — при следующем заходе не подпрыгивают.
P2 #10 — Match summary close confirm. Если в Match Summary остались
незакрытые челленджи, при close через X / Esc / btn — native
confirm с количеством остатка. Пользователь не «пролетает» окно
случайно.
P2 #11 — TTS delay 800ms. Дикторский голос ждёт 800мс прежде чем
проговорить — пользователь успевает decrement stepper если хочет
сделать меньше планового. Иначе голос произносит планируемые
reps уже когда юзер изменил цифру.
P2 #13 — Tracking badge точность. Раньше зелёный если просто
`gamesEnabled[id]`. Теперь зелёный только если live (enabled +
integrationActive + launchOptionStatus='applied'). Введён tone
'warning' для «включена, но launch option ещё queued» с подсказкой
«закрой Steam и снова открой». Dashboard подписывается на
onGamesChanged + первый listGames на mount.
P1 #4 — ConfirmModal (новый src/renderer/src/components/ui/ConfirmModal.tsx)
с iOS-стилем + focus-trap (через Modal). Delete упражнения в Dashboard
теперь спрашивает «Удалить упражнение?» с destructive-кнопкой.
P1 #5 — Daily goal closed UI. ExerciseCard принимает doneToday prop
и при `done >= dailyGoal` показывает «Цель закрыта · 100/100»
вместо запутанного «25ч 13м» countdown'а. Цвет — success-зелёный.
P1 #6 — Meeting auto-pause indicator. Новый IPC.getMeetingActive +
evtMeetingChanged event. meeting-detect broadcast'ит изменения
состояния. Dashboard показывает info-баннер «Не дёргаем — ты на
встрече» когда meetingAutoPause включён и хотя бы один meeting
процесс запущен.
P1 #7 — Native window.confirm() заменён на ConfirmModal в Settings
DataCard для restore-операции. Теперь iOS-style с destructive
confirm-кнопкой и focus-trap'ом.
Заодно P2 #8: Brain-иконка-badge на ExerciseCard для adaptive
упражнений — пользователь видит почему «Next» не строго равен
intervalMinutes.
P2 #12: dailyReps/dailyRepsRange/totalDoneReps/repsDoneTodayForExercise
используют entry.reps как fallback — heatmap не теряет данные
после удаления упражнения.
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.
- src/shared/release-notes.ts — статический реестр заметок per-version
(RU + EN), с тегами new/fix/security/perf для tint'а иконок.
- Settings.lastSeenVersion — версия, для которой пользователь видел
модалку. Валидатор регэксом /^\d+\.\d+\.\d+(-[\w.]+)?$/.
- IPC.getAppVersion → app.getVersion() для renderer'а.
- WhatsNewModal — список пунктов с цветовыми иконками. Footer-кнопка
«Понятно» / «Got it».
- App.tsx: после hydrate смотрит lastSeenVersion → current. Если
расходятся и есть пропущенные заметки → автопоказ. На первой
записи (lastSeenVersion === undefined) — тихо записываем, без
модалки, чтобы не бить нового пользователя CHANGELOG'ом.
- Settings → раздел «О приложении» → кнопка «Открыть» показывает
модалку с заметками всех релизов.
#6 sandbox: true на обоих BrowserWindow (раньше false). Preload
использует только contextBridge + ipcRenderer (оба sandbox-safe),
никаких Node-built-ins. OS-уровневый sandbox изолирует renderer
от GPU/IPC процессов; даже RCE в зависимости renderer'а не
получит Node-доступа через preload.
#17 self-host шрифтов через @fontsource/* пакеты. Раньше тянулись
с fonts.googleapis.com — внешняя CSP-зависимость + отсутствие
интернета = шрифты не загружались. Теперь .woff/.woff2 в bundle
(22 файла × 15-30KB = ~500KB).
Подкрутили CSP: убрали https://fonts.* origins, добавили
connect-src 'self', base-uri 'self', frame-ancestors 'none'.
#22 src/main/logger.ts — структурный лог с уровнями
(debug/info/warn/error) и ротацией. Пишет в
%APPDATA%/Exercise Reminder/logs/latest.log (≤1MB) и
дублирует в console. При 1MB latest.log → prev.log
(предыдущий prev.log удаляется). LAUDE_DEBUG=1 включает
debug-уровень.
Подключён в hot paths: store (corrupt/atomic write fails),
updater (silent check errors), gsi-server (bad requests,
handler throws), games/registry (GSI start, reconcile, match_end
summary), games/dota2 (rejected token, POST_GAME detection).
Особенно полезно для диагностики «челленджи не срабатывают»:
лог покажет (а) пришёл ли вообще GSI payload (token verify),
(б) детектировался ли POST_GAME, (в) сколько challenges были
enabled и которые из них дали 0 reps.
Logger — единственный файл с `eslint-disable no-console` (он
намеренно дублирует в stderr).