feat(#12): дневная цель — soft cap reps/день, после которого упражнение умолкает

This commit is contained in:
AnRil
2026-05-22 13:45:37 +07:00
parent 9e59be9cba
commit a6ae931461
6 changed files with 121 additions and 11 deletions

View File

@@ -1,11 +1,29 @@
import { powerMonitor, BrowserWindow } from 'electron'
import { IPC } from '@shared/ipc'
import type { Tick } from '@shared/types'
import type { Exercise, Tick, HistoryEntry } from '@shared/types'
import { isQuietAt } from '@shared/types'
import { getExercises, getSettings, updateExercise } from './store'
import { getExercises, getHistory, getSettings, updateExercise } from './store'
import { fireReminder } from './notifications'
import { broadcastState } from './state-actions'
/**
* Сколько reps пользователь сделал по упражнению `ex` за сегодня (local day).
* Учитываем actualReps если задано (частичное выполнение), иначе planned reps.
*/
function repsDoneToday(ex: Exercise, history: HistoryEntry[]): number {
const todayKey = new Date()
todayKey.setHours(0, 0, 0, 0)
const startMs = todayKey.getTime()
let sum = 0
for (const e of history) {
if (e.action !== 'done') continue
if (e.exerciseId !== ex.id) continue
if (e.ts < startMs) continue
sum += e.actualReps ?? ex.reps
}
return sum
}
/**
* TICK_MS drives the per-second countdown UI; CHECK_MS gates the (cheaper)
* "is anything due to fire?" pass so we don't iterate exercises every second.
@@ -29,18 +47,33 @@ function checkDueExercises(): void {
const now = Date.now()
const exercises = getExercises()
// history запрашивается только если хотя бы у одного упражнения есть
// dailyGoal — для большинства pure-interval упражнений не нужна.
const anyGoal = exercises.some((e) => e.dailyGoal !== undefined)
const history = anyGoal ? getHistory() : []
let anyFired = false
for (const ex of exercises) {
if (!ex.enabled) continue
if (ex.nextFireAt <= now) {
const updated = updateExercise(ex.id, {
nextFireAt: now + ex.intervalMinutes * 60_000
})
if (updated) {
anyFired = true
fireReminder(updated, settings.notificationMode)
if (ex.nextFireAt > now) continue
// Soft cap: если dailyGoal задан и уже выполнен — переносим
// следующий fire на «начало завтра» (без повторных проверок до утра).
if (ex.dailyGoal !== undefined && ex.dailyGoal > 0) {
const done = repsDoneToday(ex, history)
if (done >= ex.dailyGoal) {
const tomorrow = new Date()
tomorrow.setHours(0, 0, 0, 0)
tomorrow.setDate(tomorrow.getDate() + 1)
updateExercise(ex.id, { nextFireAt: tomorrow.getTime() })
continue
}
}
const updated = updateExercise(ex.id, {
nextFireAt: now + ex.intervalMinutes * 60_000
})
if (updated) {
anyFired = true
fireReminder(updated, settings.notificationMode)
}
}
// Push fresh state so the renderer's Dashboard/Exercises pages don't show
// stale `nextFireAt` until the next state-changing IPC arrives.