303 lines
19 KiB
Markdown
303 lines
19 KiB
Markdown
# Changelog
|
||
|
||
Все заметные изменения проекта документируются здесь.
|
||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
||
проект следует [Semantic Versioning](https://semver.org/lang/ru/).
|
||
|
||
## [Unreleased]
|
||
|
||
## [0.5.5] — 2026-05-22
|
||
|
||
Большой sweep по ревизии: 4 спринта правок (≈14 пунктов), все 135 тестов
|
||
зелёные. Главное — UI больше не залипает при retry'ях I/O, GSI порт не
|
||
зависает в TIME_WAIT после выхода, sandbox включён, шрифты self-hosted.
|
||
|
||
### Security
|
||
|
||
- **`sandbox: true`** на обоих BrowserWindow. Preload использует только
|
||
contextBridge + ipcRenderer (sandbox-safe), никаких Node-built-ins.
|
||
OS-уровневый sandbox изолирует renderer на уровне процессов — даже
|
||
RCE в зависимости рендерера не получит Node-доступа через preload.
|
||
- **CSP ужесточён.** Убраны `https://fonts.googleapis.com` и
|
||
`https://fonts.gstatic.com` origins (шрифты теперь self-hosted),
|
||
добавлены `connect-src 'self'`, `base-uri 'self'`,
|
||
`frame-ancestors 'none'`.
|
||
|
||
### Added
|
||
|
||
- **Self-hosted шрифты.** Plus Jakarta Sans, Bricolage Grotesque,
|
||
JetBrains Mono подключены через `@fontsource/*` пакеты — в bundle
|
||
лежат локально, без интернета шрифты работают, CSP без внешних
|
||
origins. +22 .woff/.woff2 (~500KB) в installer.
|
||
- **`src/main/logger.ts`** — структурный logger с уровнями
|
||
(debug/info/warn/error) и ротацией. Пишет в
|
||
`%APPDATA%/Exercise Reminder/logs/latest.log` (≤1MB) и дублирует
|
||
в console. При 1MB ротируется в `prev.log`. `LAUDE_DEBUG=1`
|
||
включает debug-уровень. Подключён в hot paths: store, updater,
|
||
GSI server, registry, dota2 provider — особенно полезно для
|
||
диагностики «челленджи не срабатывают» (видно token verify,
|
||
POST_GAME detection, фильтрацию challenges).
|
||
- `<html lang>` синхронизируется с `settings.language` через
|
||
ThemeProvider — screen readers корректно произносят язык.
|
||
- `dev:simulateMatchEnd` channel вынесен в IPC enum
|
||
(`IPC.devSimulateMatchEnd`).
|
||
- `test:coverage` npm script.
|
||
|
||
### Changed
|
||
|
||
- **`broadcastState` больше не шлёт `history`** через IPC. Раньше
|
||
каждый markDone/snooze отправлял весь state включая до 10k
|
||
history-записей (~500KB JSON) к каждому BrowserWindow. Теперь
|
||
`AppState` (renderer-facing) без `history`, а `PersistedState`
|
||
(internal) с историей. Renderer и так дёргал `getHistory()`
|
||
отдельно, поведение не изменилось — только perf.
|
||
- **`lib/icon.tsx`**: `import * as Lucide` (wildcard, ~500KB всех
|
||
1500+ иконок в bundle) → explicit named imports + ICON_MAP.
|
||
В bundle только 18 ICON_CHOICES.
|
||
- **ChallengeEditor**: multiplier клампится в UI до [0.5, 1000]
|
||
(совпадает с validate.ts). Раньше save с 9999 молча отклонялся
|
||
IPC-валидатором.
|
||
|
||
### Fixed
|
||
|
||
- **`atomicWrite` spin-loop → async setTimeout.** Раньше при retry
|
||
на EBUSY/EPERM (антивирус, OneDrive) main process замораживался
|
||
на 50/200/800ms × до 3 итераций ≈ секунда залипания UI. Сейчас
|
||
async sleep — event-loop живёт. Аналогичный фикс в
|
||
`games/steam-launch-options.ts`. Сохранён sync-вариант для
|
||
`flushNow` в `before-quit` (там event-loop уже не работает).
|
||
- **`before-quit` дожидается `stopGamesRegistry`** через
|
||
`e.preventDefault()` + `app.exit(0)`. Раньше GSI HTTP server
|
||
не успевал `closeAllConnections` до exit, и следующий запуск
|
||
получал EADDRINUSE на порту 4701 (TIME_WAIT) — GSI молча не
|
||
работал.
|
||
- **IPC `getState` не мутирует кэш.** Раньше `state.settings.startWithWindows`
|
||
перезаписывалось напрямую, разъезжаясь с persisted-disk-значением
|
||
до следующего mutation. Сейчас возвращается поверхностная копия.
|
||
|
||
## [0.5.4] — 2026-05-19
|
||
|
||
Обновление приложения теперь по-настоящему фоновое + почти моментальный
|
||
рестарт в новую версию.
|
||
|
||
### Changed
|
||
|
||
- **Скачивание апдейта — фоновое.** Раньше клик «Скачать» блокировал
|
||
кнопку (`busy=true`) до конца download'а (минуты на медленной сети).
|
||
Теперь IPC `updaterDownload` — fire-and-forget, прогресс приходит
|
||
через события. Пользователь сразу может уйти на Dashboard и
|
||
продолжать упражнения, апдейт качается в фоне.
|
||
- **«Рестарт» — почти моментальный.** `quitAndInstall(true, true)`:
|
||
isSilent=true — NSIS без UI установщика (~1-2 сек вместо ~5-10),
|
||
isForceRunAfter=true — гарантия что приложение откроется после.
|
||
Раньше показывался диалог установщика с прогрессом, теперь —
|
||
только мгновение между закрытием и появлением новой версии.
|
||
- Подсказка на экране скачивания: «можно закрыть это окно, продолжится
|
||
в фоне». На downloaded-экране: «нажми Рестарт — приложение
|
||
моментально откроется в новой версии».
|
||
|
||
## [0.5.3] — 2026-05-19
|
||
|
||
Полировка кастомного тайтлбара и размера окна.
|
||
|
||
### Added
|
||
|
||
- **Maximize/Restore.** Средняя кнопка тайтлбара (иконка квадрата)
|
||
раньше была «спрятать в трей» — выглядела как нативная Windows
|
||
maximize и сбивала с толку. Теперь это настоящий toggle на
|
||
весь экран: иконка свапается `Square` ↔ `Copy` в зависимости
|
||
от состояния, aria-label локализован.
|
||
- **Double-click по тайтлбару** тоже toggleMaximize — стандартный
|
||
Windows-жест.
|
||
- **CLAUDE.md** в корне — контекст проекта для будущих сессий
|
||
Claude Code (стек, архитектура, команды, тех. долг).
|
||
|
||
### Fixed
|
||
|
||
- **Drag-зона тайтлбара.** Окно не двигалось, если хватать его
|
||
рядом с кнопками свернуть/закрыть. Класс `titlebar-nodrag` стоял
|
||
на обёртке кластера с `flex-1 basis-0`, поэтому пустое место
|
||
слева от иконок тоже было no-drag. Перенесли `no-drag` на сами
|
||
кнопки — теперь тащить можно отовсюду, кроме самих квадратиков.
|
||
|
||
### Changed
|
||
|
||
- **Минимальный размер окна** 900×600 → 1100×700. Гарантирует
|
||
срабатывание Tailwind `lg:` (4 hero-stat в один ряд, heatmap
|
||
и сетка упражнений помещаются без горизонтального скролла).
|
||
|
||
## [0.5.2] — 2026-05-19
|
||
|
||
Большая внутренняя итерация: тройной независимый аудит (~220 находок),
|
||
закрыты топ-приоритеты. Тестов 53, ESLint и Prettier чистые, typecheck OK.
|
||
|
||
### Added
|
||
|
||
- **Prettier + ESLint + EditorConfig.** Конфиги, скрипты
|
||
`npm run format` / `format:check` / `lint`, CI-готовые правила. Вся
|
||
`src/` единообразно отформатирована.
|
||
- **Error Boundary** на двух уровнях: вокруг всего App и вокруг
|
||
роутов. Крах одной страницы (например, malformed history в
|
||
HistoryHeatmap) больше не блэнкит окно — показывается локализованный
|
||
fallback с кнопкой «Попробовать снова». Stack trace только в dev.
|
||
- **IPC validation layer** (`src/main/validate.ts`) — hand-rolled
|
||
схемы для всех renderer-supplied payload (intervalMinutes ∈ [1,1440],
|
||
reps ∈ [1,9999], multiplier ∈ [0,1000], string-cap 200 chars,
|
||
enum-валидация для theme/lang/notify-mode/stat, regex для HH:MM,
|
||
дедупликация quietHours.days). Compromised renderer больше не может
|
||
засунуть `reps: NaN` или `intervalMinutes: -1` в стор.
|
||
- **Schema migrations framework.** `__schemaVersion` в persisted-state,
|
||
`MIGRATIONS` map для будущих структурных правок.
|
||
- **Modal focus trap + focus restore + aria-labelledby.** Tab/Shift-Tab
|
||
больше не вываливаются на нижний слой; на закрытии фокус
|
||
возвращается на триггер.
|
||
- **Sidebar mobile drawer:** Esc закрывает, focus trap внутри, focus
|
||
restore на гамбургер, `role="dialog"` + `aria-modal`.
|
||
- **Tray menu i18n** — пункты меню следуют `settings.language`.
|
||
- **Bilingual heatmap.** Title, легенда, weekday-лейблы и tooltip
|
||
с плюрализацией (1 повтор / 2 повтора / 5 повторов) — всё через
|
||
i18n. 7 новых ключей `weekday.short.*`.
|
||
- CHANGELOG.md по формату Keep a Changelog.
|
||
|
||
### Fixed
|
||
|
||
- **Critical: данные больше не теряются на corrupt JSON.** Раньше
|
||
`catch → makeInitial()` молча затирал упражнения/историю. Теперь
|
||
файл уезжает в `app-state.json.corrupt-<timestamp>`.
|
||
- **Atomic write через `.tmp` + rename + retry** на EBUSY/EPERM
|
||
(антивирус, OneDrive). Раньше обрыв питания мог дать truncate.
|
||
- **HIGH security: GSI server теперь верифицирует auth.token**
|
||
через `timingSafeEqual` против per-install токена. Раньше
|
||
эндпоинт был полностью неаутентифицирован — любой локальный
|
||
процесс мог подделать match-end.
|
||
- **HIGH security: `shell.openExternal` allowlist** —
|
||
только `http/https/mailto`. Раньше `file:`/`javascript:`/`steam:`
|
||
уходили в OS handler.
|
||
- **HIGH security: dev IPC `simulateMatchEnd`** убран из production
|
||
билдов (gate на `!app.isPackaged` + `import.meta.env.MODE`).
|
||
- **HIGH security: GSI server reject `Origin`/`Sec-Fetch-Site`** —
|
||
блокирует CSRF от browser-вкладок. Body cap 256 KB (OOM-вектор
|
||
закрыт). Require `application/json`. Generic 400 без error-echo.
|
||
- **`isQuietAt` wrap-around + day filter.** С `22:00 → 07:00,
|
||
days=[Mon..Fri]` теперь правильно проверяется день *начала* окна
|
||
(старт Fri 22:00 → активно ночью Sat 02:00).
|
||
- **DST drift в `history.ts`.** Календарная арифметика (`setDate`)
|
||
вместо ms-арифметики — на границе DST дни больше не дублируются.
|
||
- **Scheduler:** `broadcastState()` после fire, защита от
|
||
двойной регистрации `powerMonitor` listeners.
|
||
- **Settings IPC chatter.** QuietTimesRow держит локальное состояние,
|
||
IPC летит только на `onBlur`. Раньше скрабинг времени давал ~5
|
||
IPC, каждый переписывал `app-state.json`.
|
||
- **Dashboard** «До следующего» показывает `—` при паузе вместо
|
||
обманчиво тикающего таймера.
|
||
- **HistoryHeatmap** percentile-bucketing (p25/p50/p85) вместо
|
||
относительной шкалы — outlier-день больше не схлопывает все
|
||
нормальные дни в самый слабый бакет.
|
||
- **ReminderApp:** Enter теперь корректно передаёт adjusted reps
|
||
(раньше всегда planned). `key={exercise.id+nextFireAt}` сбрасывает
|
||
степпер на новом fire. Степпер capped at 5× planned. Space не
|
||
работает когда фокус на кнопке. Esc закрывает MatchSummary.
|
||
- **`i18n.translate`** — split/join вместо regex (var-значения с
|
||
регулярными метасимволами теперь интерполируются буквально).
|
||
- **`icon.tsx`** lookup сужен до `ICON_CHOICES` — произвольное имя
|
||
больше не зарезолвится в `Lucide.default`.
|
||
- **UpdaterCard NaN guard** на download-progress (electron-updater
|
||
даёт undefined в ранних событиях).
|
||
- **`format.ts`** guard от NaN/Infinity в `formatCountdown`.
|
||
- **`updateExercise`/`updateChallenge`** стрипают `id` из patch —
|
||
рендер не может перезаписать identity.
|
||
- **clearHistory(undefined)** теперь no-op (нужен явный boundary).
|
||
|
||
### Removed
|
||
|
||
- `.gitea/workflows/*.yml` — без runners оставляли queued runs.
|
||
Релизим через `release.ps1`. has_actions на репо выключен.
|
||
|
||
## [0.5.1] — 2026-05-18
|
||
|
||
### Fixed
|
||
|
||
- **Auto-update архитектурно переписан.** Раньше `publish.url` включал
|
||
`${version}` и запекался в каждый билд — установленные копии видели
|
||
только свой собственный релиз. Введён фиксированный
|
||
`…/releases/download/update-channel`, который никогда не меняется.
|
||
- Hourly auto-проверка работает в silent-режиме: транзитные сетевые
|
||
ошибки (504, TLS drops) больше не показывают красный баннер
|
||
«Ошибка проверки». Только ручной клик «Проверить» поднимает ошибку.
|
||
- Boot-check ретраит 3 раза с backoff 30s/2m/5m.
|
||
- В `Up to date` показывается «проверено N мин назад».
|
||
- `release.ps1` теперь публикует в три-четыре места одной командой:
|
||
vX.Y.Z, update-channel, и переданные `-BridgeTags` для миграции
|
||
пользователей со старых версий.
|
||
- `upload-release-assets.ps1` ретраит curl до 4 раз с backoff на 504 /
|
||
TLS-сбрасывание; до ретрая проверяет, не залился ли файл на самом
|
||
деле (Gitea часто принимает body, но таймаутит ответ).
|
||
- Скрипты — ASCII-only (PS5.1 без BOM падает на em-dash).
|
||
|
||
### Removed
|
||
|
||
- `.gitea/workflows/*.yml` — Gitea Actions без настроенных runners
|
||
оставляли queued runs в репозитории. Релизим через `release.ps1`.
|
||
|
||
## [0.5.0] — 2026-05-18
|
||
|
||
### Added
|
||
|
||
- **История + стрики.** Каждое выполненное упражнение пишется в
|
||
`app-state.json` (cap 10k записей, trim oldest 10% на overflow).
|
||
Heatmap-календарь 12 недель на Dashboard, ежедневный счётчик
|
||
«сделано сегодня», серия дней подряд (с grace-периодом за вчера).
|
||
- **Тихие часы.** Окно времени, в которое напоминания подавляются.
|
||
Поддержка wrap-around (22:00 → 08:00) и фильтра по дням недели.
|
||
- **Частичное выполнение.** Степпер `−/+` в окне напоминания: можно
|
||
отметить «сделал 5 из 10», в историю запишется честное число.
|
||
- README.md на русском — описание, фичи, установка, dev-команды,
|
||
архитектура, стек.
|
||
|
||
### Changed
|
||
|
||
- `markDone(id, actualReps?)` принимает фактическое число повторений.
|
||
|
||
### Tests
|
||
|
||
- `+18` тестов (5 для тихих часов, 13 для истории/стриков). Всего 51.
|
||
|
||
## [0.4.0] — 2026-05-17
|
||
|
||
### Added
|
||
|
||
- **Английская локализация.** Самописная i18n: плоский словарь
|
||
~200 ключей × 2 языка + хук `useT()` + плюрализация (CLDR rules
|
||
для RU: one/few/many).
|
||
- Селектор языка в Settings, переключение мгновенное.
|
||
|
||
## [0.3.x] — 2026-05-17
|
||
|
||
Серия мелких релизов с дизайн-итерациями (Apple iOS / macOS aesthetic):
|
||
шрифты Plus Jakarta Sans + Bricolage Grotesque, светлая/тёмная/системная
|
||
тема, vibrancy sidebar, iOS-grouped lists, spring-анимации.
|
||
|
||
## [0.2.0] — 2026-05-16
|
||
|
||
### Added
|
||
|
||
- Dota 2 Game State Integration: локальный HTTP-сервер парсит callbacks
|
||
от Steam, после Победа/Поражение показывает «причитающиеся»
|
||
повторения (например `10 смертей × 3 = 30 приседаний`).
|
||
|
||
## [0.1.x] — 2026-05-15 .. 2026-05-16
|
||
|
||
Первые публичные сборки: ядро напоминаний (упражнения, интервалы,
|
||
иконки), системный трей, автозапуск с Windows, native-уведомления,
|
||
NSIS-инсталлятор, auto-update через electron-updater.
|
||
|
||
[Unreleased]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/compare/v0.5.5...HEAD
|
||
[0.5.5]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.5.5
|
||
[0.5.4]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.5.4
|
||
[0.5.3]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.5.3
|
||
[0.5.2]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.5.2
|
||
[0.5.1]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.5.1
|
||
[0.5.0]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.5.0
|
||
[0.4.0]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.4.0
|
||
[0.2.0]: https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/tag/v0.2.0
|