/** * Заметки релизов для UI «Что нового». Хардкодные — синхронизируются * с CHANGELOG.md руками при релизе. Не парсим .md в runtime: лишняя * сложность, плюс отделение «коротких заметок для пользователя» от * «развёрнутого технического CHANGELOG'а». * * Формат: версия → язык → массив пунктов (короткий заголовок + опц. деталь). * Не более 5-7 пунктов на версию, иначе пользователь скроллит. */ import type { Language } from './types' export type ReleaseNoteItem = { /** Лаконичная строка-заголовок (≤ 60 символов). */ title: string /** Опциональная одна строка-деталь (≤ 140 символов). */ detail?: string /** Категория для tint'а иконки. */ tag?: 'new' | 'fix' | 'security' | 'perf' } export type ReleaseNotes = Record export const RELEASE_NOTES: Record = { '0.6.6': { ru: [ { title: 'Настройки стали панелью состояния', detail: 'Сверху видно, работают ли напоминания, включены ли тихие часы, встречи и запуск вместе с Windows.', tag: 'new' }, { title: 'Главный экран стал точнее по текстам', detail: 'Дата больше не кричит заглавными буквами, а цели показывают понятные единицы: “осталось 30 раз”.', tag: 'fix' }, { title: 'Сводные карточки читаются лучше', detail: 'Длинные значения в карточках больше не обрезаются, а сетка спокойнее держит десктопные ширины.', tag: 'fix' }, { title: 'Безопасный стенд для проверки интерфейса', detail: 'Добавлен dev:renderer: можно открыть UI в браузере с демо-данными и не трогать реальные настройки.', tag: 'new' } ], en: [ { title: 'Settings now show app status first', detail: 'The top panel shows whether reminders, quiet hours, meeting pause and Windows autostart are active.', tag: 'new' }, { title: 'Overview copy is clearer', detail: 'The date no longer gets artificial capitalization, and goals show units like “30 reps left”.', tag: 'fix' }, { title: 'Summary cards are easier to read', detail: 'Long values no longer get clipped, and the card grid behaves better on desktop widths.', tag: 'fix' }, { title: 'Safe renderer preview for UI checks', detail: 'Added dev:renderer so the UI can be opened with demo data without touching real settings.', tag: 'new' } ] }, '0.6.5': { ru: [ { title: 'Главный экран стал обзором действий', detail: 'Верхний заголовок теперь показывает состояние: что сделать сейчас, что ждёт, пауза или встреча.', tag: 'new' }, { title: 'Исправлен запуск с Windows', detail: 'Проверка автозапуска теперь использует тот же путь и аргументы, что и запись в Windows.', tag: 'fix' }, { title: 'Discord больше не ставит перерывы на паузу', detail: 'Авто-пауза встреч реагирует на Zoom, Teams, Webex и Slack-huddle, но не на обычный Discord.', tag: 'fix' } ], en: [ { title: 'The main screen is now an action overview', detail: 'The header shows the current state: what to do now, what is due, pause or meeting.', tag: 'new' }, { title: 'Fixed Start with Windows', detail: 'Autostart now reads Windows login items with the same path and arguments it writes.', tag: 'fix' }, { title: 'Discord no longer pauses breaks', detail: 'Meeting auto-pause still handles Zoom, Teams, Webex and Slack-huddle, but ignores Discord.', tag: 'fix' } ] }, '0.6.4': { ru: [ { title: 'Новый видимый бренд “Разомнись”', detail: 'Название стало короче и понятнее для русской аудитории: действие видно сразу.', tag: 'new' }, { title: 'Сводки на каждом экране', detail: 'Упражнения, питание, игры, челленджи и настройки теперь быстрее сканируются сверху.', tag: 'new' }, { title: 'Улучшена структура вторичных страниц', detail: 'Важные статусы вынесены выше списков, чтобы сразу видеть активность и проблемные места.', tag: 'new' } ], en: [ { title: 'New visible “Razomnis” brand', detail: 'The name is shorter and more action-focused for the Russian audience.', tag: 'new' }, { title: 'Overview cards on every screen', detail: 'Exercises, meals, games, challenges and settings are easier to scan from the top.', tag: 'new' }, { title: 'Secondary pages are more structured', detail: 'Important statuses moved above long lists so active and risky areas are visible immediately.', tag: 'new' } ] }, '0.6.3': { ru: [ { title: 'Новый главный экран “Не Залипай”', detail: 'Сегодня теперь показывает не только план, но и недельный ритм, уровень и игровые долги.', tag: 'new' }, { title: 'Мини-челленджи недели', detail: '5 дней без нуля, 1000 повторов, “сегодня не ноль” и закрытые катки считаются автоматически.', tag: 'new' }, { title: 'Игровой долг после каток', detail: 'На Dashboard видно, сколько Dota-долгов закрыто сегодня и за неделю.', tag: 'new' }, { title: 'Мягкая система уровней', detail: 'XP считается из повторов, активных дней и закрытых игровых челленджей.', tag: 'new' } ], en: [ { title: 'New “Ne Zalipay” Today screen', detail: 'Today now shows the day plan, weekly rhythm, level and game debts.', tag: 'new' }, { title: 'Weekly mini-challenges', detail: '5 non-zero days, 1000 reps, “today is not zero” and closed matches are tracked automatically.', tag: 'new' }, { title: 'Game debt after matches', detail: 'Dashboard shows how many Dota debts were closed today and this week.', tag: 'new' }, { title: 'Soft level system', detail: 'XP comes from reps, active days and completed game challenges.', tag: 'new' } ] }, '0.5.8': { ru: [ { title: 'Heatmap и стрик обновляются сразу после нажатия «Готово»', detail: 'Был регресс — статистика не обновлялась пока не изменишь упражнение. Починено через новое событие evtHistoryChanged.', tag: 'fix' }, { title: 'Двойной клик на ✓ больше не пишет 2 раза', detail: 'Rapid double-click на ✓ в Match Summary и «Готово» давал лишние повторы в стрик. Добавлен ref-based дедуп.', tag: 'fix' }, { title: 'Native save/open dialogs локализованы', tag: 'fix' }, { title: '+18 тестов на новые модули', detail: 'achievements, match-challenge edge cases, deleted exercise survival.', tag: 'new' } ], en: [ { title: 'Heatmap and streak update immediately after pressing "Done"', detail: 'There was a regression — stats did not update until you edited an exercise. Fixed via new evtHistoryChanged event.', tag: 'fix' }, { title: 'Double-click on ✓ no longer writes twice', detail: 'Rapid double-click on ✓ in Match Summary and "Done" added extra reps to streak. ref-based dedup.', tag: 'fix' }, { title: 'Native save/open dialogs localised', tag: 'fix' }, { title: '+18 tests for new modules', detail: 'achievements, match-challenge edge cases, deleted exercise survival.', tag: 'new' } ] }, '0.5.7': { ru: [ { title: 'Челленджи из матчей идут в историю', detail: 'Раньше ✓ в Match Summary не считался — стрик и достижения игнорировали игровые тренировки. Исправлено.', tag: 'fix' }, { title: 'Пауза из трея и из Dashboard теперь синхронизированы', detail: 'Раньше Dashboard показывал «running» когда tray был на паузе.', tag: 'fix' }, { title: 'Удаление упражнения спрашивает подтверждение', tag: 'fix' }, { title: 'Daily goal: «Цель закрыта · 100/100»', detail: 'Когда дневная цель достигнута — больше не показываем обратный отсчёт «25ч 13м».', tag: 'fix' }, { title: 'Видно когда мы молчим из-за ВКС', detail: 'Запущен Zoom/Teams — на Dashboard появляется баннер активной встречи.', tag: 'new' }, { title: 'Celebration анимация на новых достижениях', tag: 'new' }, { title: 'Tracking-badge точнее', detail: 'Live / Setup (закрой Steam) / Off — раньше зелёный показывался даже когда launch option не применён.', tag: 'fix' } ], en: [ { title: 'Match challenges now write to history', detail: 'Previously ✓ in Match Summary did not count — streak and achievements ignored game training. Fixed.', tag: 'fix' }, { title: 'Tray pause and Dashboard pause are now synced', detail: "Previously Dashboard showed 'running' while tray was paused.", tag: 'fix' }, { title: 'Exercise deletion asks for confirmation', tag: 'fix' }, { title: 'Daily goal: "Goal hit · 100/100"', detail: 'When the daily goal is met — no more confusing 25h countdown to tomorrow.', tag: 'fix' }, { title: "Visible when we're quiet because of a meeting", detail: "Zoom/Teams running — Dashboard shows a banner: 'You're in a meeting'.", tag: 'new' }, { title: 'Celebration animation on newly unlocked achievements', tag: 'new' }, { title: 'Tracking badge more accurate', detail: 'Live / Setup (close Steam) / Off — previously green even when launch option was not applied.', tag: 'fix' } ] }, '0.5.6': { ru: [ { title: 'Категории напоминаний', detail: 'Помимо упражнений — гидратация, отдых глазам (20-20-20), осанка.', tag: 'new' }, { title: 'Голосовые подсказки', detail: 'Диктор произносит название упражнения и количество. Включается в настройках.', tag: 'new' }, { title: 'Достижения', detail: 'Milestones по количеству повторений и серий — с прогресс-баром до ближайшей цели.', tag: 'new' }, { title: 'Дневная цель', detail: 'Soft-cap на упражнение: набрал N повторов — реминдер умолкает до завтра.', tag: 'new' }, { title: 'Авто-пауза на ВКС', detail: 'Ставит напоминания на паузу, если запущен Zoom/Teams/Webex/Slack-huddle.', tag: 'new' }, { title: 'Адаптивный шедулер', detail: 'Учит часы, в которые ты честно делаешь упражнение, и сдвигает fire-ы туда.', tag: 'new' }, { title: 'Экспорт и импорт', detail: 'Резервная копия упражнений, истории и настроек в JSON — для переноса на другую машину.', tag: 'new' }, { title: 'Кнопка «Что нового»', detail: 'Это окно. Открывается автоматически после обновления.', tag: 'new' } ], en: [ { title: 'Reminder categories', detail: 'Beyond exercises — hydration, eye rest (20-20-20), posture.', tag: 'new' }, { title: 'Voice prompts', detail: 'Speaks the exercise name and count. Toggle in Settings.', tag: 'new' }, { title: 'Achievements', detail: 'Milestones by total reps and streaks, with a progress bar to the next one.', tag: 'new' }, { title: 'Daily goal', detail: 'Soft cap per exercise: hit N reps and that reminder goes quiet until tomorrow.', tag: 'new' }, { title: 'Meeting auto-pause', detail: 'Pauses reminders while Zoom/Teams/Webex/Slack-huddle is running.', tag: 'new' }, { title: 'Adaptive scheduling', detail: 'Learns the hours you reliably do an exercise and shifts fires into those windows.', tag: 'new' }, { title: 'Export & import', detail: 'JSON backup of exercises, history and settings — for moving to another machine.', tag: 'new' }, { title: "What's new screen", detail: 'This screen. Opens automatically after an update.', tag: 'new' } ] }, '0.5.5': { ru: [ { title: 'Sandbox для окон', detail: 'Окна изолированы на уровне OS — даже RCE в рендере не достанет main.', tag: 'security' }, { title: 'Self-hosted шрифты', detail: 'Plus Jakarta Sans + Bricolage Grotesque локально в bundle — работает без интернета.', tag: 'security' }, { title: 'Логи для диагностики', detail: '%APPDATA%/Exercise Reminder/logs/latest.log — отдашь файл при проблеме.', tag: 'new' }, { title: 'UI не залипает при retry-ях I/O', tag: 'perf' }, { title: 'GSI больше не зависает на TIME_WAIT при выходе', tag: 'fix' } ], en: [ { title: 'Window sandbox', detail: 'OS-level isolation — even RCE in the renderer cannot reach main.', tag: 'security' }, { title: 'Self-hosted fonts', detail: 'Plus Jakarta Sans + Bricolage Grotesque shipped in-bundle — works offline.', tag: 'security' }, { title: 'Diagnostic logs', detail: '%APPDATA%/Exercise Reminder/logs/latest.log — share it when something breaks.', tag: 'new' }, { title: 'UI no longer freezes during I/O retries', tag: 'perf' }, { title: 'GSI port no longer stuck in TIME_WAIT after exit', tag: 'fix' } ] }, '0.5.4': { ru: [ { title: 'Фоновое скачивание апдейта', detail: 'Можно уйти на Dashboard и заниматься — апдейт качается в фоне.', tag: 'new' }, { title: 'Моментальный рестарт', detail: 'Кнопка «Рестарт» — ~1-2 сек до открытия новой версии, без диалогов NSIS.', tag: 'new' } ], en: [ { title: 'Background update download', detail: 'You can go to Dashboard and work — the update keeps downloading.', tag: 'new' }, { title: 'Instant restart', detail: 'Restart button — ~1-2 sec to the new version, no NSIS dialogs.', tag: 'new' } ] } } /** * Возвращает версии, отсортированные desc, для которых есть заметки, * с версиями, не виденными пользователем. Используется для «show whatsnew * after update»: если пользователь прыгнул через несколько версий, показываем * все пропущенные одним списком. */ export function unseenVersions( current: string, lastSeen: string | undefined ): string[] { const all = Object.keys(RELEASE_NOTES).sort(compareSemverDesc) if (!lastSeen) { // Первый запуск этого механизма — показываем только текущую версию // чтобы не перегружать историей. Старые версии показывает только // явный «What's new» из Settings. return all.filter((v) => v === current) } return all.filter( (v) => compareSemver(v, lastSeen) > 0 && compareSemver(v, current) <= 0 ) } function parseSemver(v: string): [number, number, number] { const parts = v.split('.').map((n) => parseInt(n, 10)) return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0] } function compareSemver(a: string, b: string): number { const [a1, a2, a3] = parseSemver(a) const [b1, b2, b3] = parseSemver(b) if (a1 !== b1) return a1 - b1 if (a2 !== b2) return a2 - b2 return a3 - b3 } function compareSemverDesc(a: string, b: string): number { return -compareSemver(a, b) }