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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user