feat(v0.5.0): history + streak + heatmap, quiet hours, partial reps, README
== История и стрики (#1) == - HistoryEntry { ts, exerciseId, action: done|skip|snooze, actualReps? } персистится в app-state.json, лимит 10k записей (~3 года), trim oldest 10% - markDone/snooze/skip пишут в историю; markDone принимает optional actualReps - IPC: getHistory(sinceMs?), clearHistory(beforeTs?) + preload bindings - Renderer helpers (src/renderer/src/lib/history.ts): * dayKey(ts) — YYYY-MM-DD local * dailyReps(entries, exs, dayKey) — суммирует actualReps || planned * dailyRepsRange(entries, exs, days) — для heatmap, заполняет gaps нулями * currentStreak(entries) — consecutive days, today или yesterday (grace) - Dashboard теперь 4 hero-карточки: Today (повторов за день) / Streak (дней подряд) / Next / Tracking - Новый компонент HistoryHeatmap — GitHub-style 12-недельный календарь с 5 интенсивностями, локализованными подписями дней/месяцев == Тихие часы (#2) == - shared/types.ts: QuietHours { enabled, from, to, days[] } + isQuietAt() helper с правильной обработкой wrap-around окон (22:00→08:00) - DEFAULT_SETTINGS.quietHours = disabled, 22:00→08:00, все дни - main/scheduler.ts: проверка isQuietAt перед fire; deferred fires поднимаются после окончания окна - Settings UI: новая секция "Тихие часы" с toggle, time-pickers, day-of-week pills == Сделал частично (#3) == - ReminderApp: stepper [−][число][+] вокруг счётчика повторов - При adjusted (actualReps !== exercise.reps) число подсвечивается accent и появляется подпись "Засчитаем X из Y" - markDone передаёт actualReps только если юзер реально изменил — иначе undefined чтобы история фиксировала планируемое значение чисто == README.md (#4) == - Описание, фичи, скриншоты (TODO-плейсхолдер), установка, dev-команды, архитектура, тесты, stack, ссылка на RELEASING.md - Бэйджи version / tests / platform == i18n == - ~14 новых ключей × 2 языка: dashboard.stat.today_done, streak, settings.quiet.* (3 row'а), reminder.partial == Тесты — 51 (было 33) == - shared/quiet-hours.test.ts (5): disabled, same-day, wrap-around, day filtering, zero-length - renderer/lib/history.test.ts (13): dayKey, dailyReps (planned vs actual vs ignore non-done), currentStreak (empty, today gap, consecutive, yesterday grace, multi-entry same day), dailyRepsRange Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,19 @@ export type NotificationMode = 'toast' | 'modal' | 'both'
|
||||
export type Theme = 'light' | 'dark' | 'system'
|
||||
export type Language = 'ru' | 'en'
|
||||
|
||||
/**
|
||||
* Hours when reminders are silenced. `from`/`to` are "HH:MM" 24h strings,
|
||||
* `days` are weekday indices 0=Sun..6=Sat. Empty `days` = applies every day.
|
||||
* If `to <= from` the window wraps across midnight (e.g. 22:00 → 07:00).
|
||||
*/
|
||||
export type QuietHours = {
|
||||
enabled: boolean
|
||||
from: string
|
||||
to: string
|
||||
/** Days when the quiet window is active. */
|
||||
days: number[]
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
globalEnabled: boolean
|
||||
notificationMode: NotificationMode
|
||||
@@ -23,6 +36,7 @@ export type Settings = {
|
||||
theme: Theme
|
||||
language: Language
|
||||
snoozeMinutes: number
|
||||
quietHours: QuietHours
|
||||
}
|
||||
|
||||
export type AppState = {
|
||||
@@ -30,6 +44,18 @@ export type AppState = {
|
||||
settings: Settings
|
||||
challenges: Challenge[]
|
||||
gamesEnabled: Partial<Record<GameId, boolean>>
|
||||
history?: HistoryEntry[]
|
||||
}
|
||||
|
||||
export type HistoryAction = 'done' | 'skip' | 'snooze'
|
||||
|
||||
export type HistoryEntry = {
|
||||
/** ms epoch */
|
||||
ts: number
|
||||
exerciseId: string
|
||||
action: HistoryAction
|
||||
/** When user did less than planned. Only meaningful for `done`. */
|
||||
actualReps?: number
|
||||
}
|
||||
|
||||
export type Tick = {
|
||||
@@ -141,7 +167,36 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
startMinimized: false,
|
||||
theme: 'light',
|
||||
language: 'ru',
|
||||
snoozeMinutes: 5
|
||||
snoozeMinutes: 5,
|
||||
quietHours: {
|
||||
enabled: false,
|
||||
from: '22:00',
|
||||
to: '08:00',
|
||||
days: [0, 1, 2, 3, 4, 5, 6]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `now` falls inside the quiet window. Handles wrap-around
|
||||
* windows (e.g. 22:00 → 08:00). Exposed from shared so both main scheduler
|
||||
* and renderer settings UI can use the same logic.
|
||||
*/
|
||||
export function isQuietAt(qh: QuietHours, now: Date): boolean {
|
||||
if (!qh.enabled) return false
|
||||
const dow = now.getDay() // 0..6
|
||||
if (qh.days.length > 0 && !qh.days.includes(dow)) return false
|
||||
const [fh, fm] = qh.from.split(':').map(Number)
|
||||
const [th, tm] = qh.to.split(':').map(Number)
|
||||
const cur = now.getHours() * 60 + now.getMinutes()
|
||||
const fromMin = fh * 60 + fm
|
||||
const toMin = th * 60 + tm
|
||||
if (fromMin === toMin) return false
|
||||
if (fromMin < toMin) {
|
||||
// Same-day window.
|
||||
return cur >= fromMin && cur < toMin
|
||||
}
|
||||
// Wraps midnight: active if after `from` today OR before `to` today.
|
||||
return cur >= fromMin || cur < toMin
|
||||
}
|
||||
|
||||
export const SAMPLE_EXERCISES: Omit<Exercise, 'id' | 'nextFireAt'>[] = [
|
||||
|
||||
Reference in New Issue
Block a user