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:
71
src/shared/quiet-hours.test.ts
Normal file
71
src/shared/quiet-hours.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isQuietAt, type QuietHours } from './types'
|
||||
|
||||
function at(iso: string): Date {
|
||||
return new Date(iso)
|
||||
}
|
||||
|
||||
const ALL_DAYS = [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
describe('isQuietAt', () => {
|
||||
it('returns false when disabled', () => {
|
||||
const qh: QuietHours = {
|
||||
enabled: false,
|
||||
from: '00:00',
|
||||
to: '23:59',
|
||||
days: ALL_DAYS
|
||||
}
|
||||
expect(isQuietAt(qh, at('2026-05-17T12:00:00'))).toBe(false)
|
||||
})
|
||||
|
||||
it('same-day window: inside is quiet, outside is not', () => {
|
||||
const qh: QuietHours = {
|
||||
enabled: true,
|
||||
from: '13:00',
|
||||
to: '14:00',
|
||||
days: ALL_DAYS
|
||||
}
|
||||
expect(isQuietAt(qh, at('2026-05-17T13:30:00'))).toBe(true)
|
||||
expect(isQuietAt(qh, at('2026-05-17T12:59:00'))).toBe(false)
|
||||
expect(isQuietAt(qh, at('2026-05-17T14:00:00'))).toBe(false) // exclusive end
|
||||
})
|
||||
|
||||
it('wrap-around window 22:00 → 08:00', () => {
|
||||
const qh: QuietHours = {
|
||||
enabled: true,
|
||||
from: '22:00',
|
||||
to: '08:00',
|
||||
days: ALL_DAYS
|
||||
}
|
||||
expect(isQuietAt(qh, at('2026-05-17T23:00:00'))).toBe(true)
|
||||
expect(isQuietAt(qh, at('2026-05-17T02:00:00'))).toBe(true)
|
||||
expect(isQuietAt(qh, at('2026-05-17T07:59:00'))).toBe(true)
|
||||
expect(isQuietAt(qh, at('2026-05-17T08:00:00'))).toBe(false)
|
||||
expect(isQuietAt(qh, at('2026-05-17T15:00:00'))).toBe(false)
|
||||
expect(isQuietAt(qh, at('2026-05-17T21:59:00'))).toBe(false)
|
||||
})
|
||||
|
||||
it('day filtering: window inactive on excluded days', () => {
|
||||
const qh: QuietHours = {
|
||||
enabled: true,
|
||||
from: '13:00',
|
||||
to: '14:00',
|
||||
days: [1, 2, 3, 4, 5] // weekdays only
|
||||
}
|
||||
// 2026-05-17 is Sunday (day 0)
|
||||
expect(isQuietAt(qh, at('2026-05-17T13:30:00'))).toBe(false)
|
||||
// 2026-05-18 is Monday (day 1)
|
||||
expect(isQuietAt(qh, at('2026-05-18T13:30:00'))).toBe(true)
|
||||
})
|
||||
|
||||
it('zero-length window (from === to) is never quiet', () => {
|
||||
const qh: QuietHours = {
|
||||
enabled: true,
|
||||
from: '12:00',
|
||||
to: '12:00',
|
||||
days: ALL_DAYS
|
||||
}
|
||||
expect(isQuietAt(qh, at('2026-05-17T12:00:00'))).toBe(false)
|
||||
expect(isQuietAt(qh, at('2026-05-17T12:00:01'))).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user