fix(P0): match-history, tray/dashboard pause sync, whatsnew для upgraders

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.
This commit is contained in:
AnRil
2026-05-22 14:49:29 +07:00
parent 77007636df
commit 17df87b3aa
10 changed files with 138 additions and 43 deletions

View File

@@ -199,20 +199,32 @@ function load(): PersistedState {
return coerce(runMigrations(parsed))
}
type AppendOpts = {
actualReps?: number
/** Planned reps snapshot — иначе после удаления упражнения теряем reps. */
reps?: number
/** Snapshot названия — для будущего log-view (необязательно). */
name?: string
/** 'reminder' (default) или 'match'. */
source?: import('@shared/types').HistorySource
}
function appendHistory(
exerciseId: string,
action: HistoryAction,
actualReps?: number
opts: AppendOpts = {}
): void {
const state = getState()
if (!state.history) state.history = []
const entry: HistoryEntry = { ts: Date.now(), exerciseId, action }
if (actualReps !== undefined) entry.actualReps = actualReps
if (opts.actualReps !== undefined) entry.actualReps = opts.actualReps
if (opts.reps !== undefined) entry.reps = opts.reps
if (opts.name !== undefined) entry.name = opts.name
if (opts.source !== undefined) entry.source = opts.source
state.history.push(entry)
if (state.history.length > HISTORY_MAX) {
state.history = state.history.slice(-Math.floor(HISTORY_MAX * 0.9))
}
// Caller schedules the write; appendHistory itself is internal.
}
export function getHistory(sinceMs?: number): HistoryEntry[] {
@@ -425,7 +437,12 @@ export function markDone(
if (!ex) return undefined
ex.lastDoneAt = Date.now()
ex.nextFireAt = Date.now() + ex.intervalMinutes * 60_000
appendHistory(id, 'done', actualReps)
appendHistory(id, 'done', {
actualReps,
reps: ex.reps,
name: ex.name,
source: 'reminder'
})
scheduleWrite()
return ex
}
@@ -435,7 +452,7 @@ export function snooze(id: string, minutes: number): Exercise | undefined {
const ex = state.exercises.find((e) => e.id === id)
if (!ex) return undefined
ex.nextFireAt = Date.now() + minutes * 60_000
appendHistory(id, 'snooze')
appendHistory(id, 'snooze', { reps: ex.reps, name: ex.name })
scheduleWrite()
return ex
}
@@ -445,11 +462,28 @@ export function skip(id: string): Exercise | undefined {
const ex = state.exercises.find((e) => e.id === id)
if (!ex) return undefined
ex.nextFireAt = Date.now() + ex.intervalMinutes * 60_000
appendHistory(id, 'skip')
appendHistory(id, 'skip', { reps: ex.reps, name: ex.name })
scheduleWrite()
return ex
}
/**
* Записать выполнение челленджа из match summary в историю. Не привязано
* к конкретному Exercise (челлендж может ссылаться на упражнение, которое
* пользователь даже не создал). Используем синтетический id 'challenge:<id>'.
*/
export function markChallengeDone(challengeId: string, reps: number): void {
const state = getState()
const ch = state.challenges.find((c) => c.id === challengeId)
appendHistory(`challenge:${challengeId}`, 'done', {
actualReps: reps,
reps,
name: ch?.exerciseName ?? ch?.name,
source: 'match'
})
scheduleWrite()
}
export function flushNow(): void {
if (pendingWrite) {
clearTimeout(pendingWrite)