Saturday/Sunday previously kept showing the workday countdown even
though no one's working — UX surfaced friction where there was
nothing to count. Now ClockoutView switches to a purple
weekend-hero card with "🌴 今天是周末 · 去做你想做的事" copy.
Implementation:
- WorkerStore.isWeekend computed (Sun=1, Sat=7)
- ClockoutView.body branches: weekendHero + weekendTipText for
weekend, original countdown+progress+controls for weekday
- Clockout celebration logic still runs in the background — if
someone does work Saturday and crosses their configured clockout
hour, the next-weekday view will see the dedupe key updated;
no spurious extra fire
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior: clockoutRemainingSec rolled forward to tomorrow the moment
target <= now, so the panel jumped 1s → 86399s with no observable
"zero" — the most expected moment of the day was silent.
Now:
- clockoutRemainingSec returns 0 when past today's clockout (sits
at 0 until midnight rolls the calendar component)
- WorkerStore detects the >0 → 0 edge in tickFire, fires per-day-
deduped celebration (notification + Glass × 3 + panel banner ~5s)
- SoundPlayer.playClockoutCelebration: Glass.aiff × 3 with 0.6s gap
(joyful 1.8s shot, not the sit Morse's 15s nag)
- ClockoutView overlays a lime→tomato gradient banner driven by
store.clockoutCelebrationActive
- tipText rewrite — old text said "不会响铃" which contradicts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior path used a lastFired-timestamp dedupe but never reset the
sitAccumActiveSec counter, so after the first 45min alert the
panel kept showing "已坐 90 分钟" / "135 分钟" — counter monotonic
across cycles. The UX intent is each ack starts a fresh cycle.
Now: on fire, alert + reset counter to 0. Next triggerSec of
sitting kicks a fresh alert. Reset is the simpler dedupe — there's
no physical way to re-fire without accumulating triggerSec from 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The UN notification ding is one short beep — easy to miss in a
meeting. For sit alerts, repeat the system Morse tone (0.7s
staccato "S O S") at 1Hz for 15 ticks, layered on top of the
notification + dock bounce.
- new: SoundPlayer.swift — NSSound-based repeating tone with stop()
- WorkerStore.tickFire: trigger SoundPlayer.playMorseSitAlert(count: 15)
alongside the existing notify() call
- WorkerStore.sitStop: silence in-flight alert when user stops monitoring
Uses /System/Library/Sounds/Morse.aiff — shipped on every macOS,
no resource bundling needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two P0 issues from 2026-05-19 code review:
1. PomodoroView progress ring used pomodoroFocusMin*60 as denominator
in paused state. If the user paused mid-break, ring rendered 80%
done when the underlying break was actually 40% done. Now the
store exposes pomodoroPhaseTotalSec which reads pausedPhase to
pick the right denominator.
2. WorkerNotificationCenter set its own isAuthorized but never wrote
WorkerStore.notificationsAuthorized — the top-bar notif status dot
stayed dim even after the user authorized. Refactored into a single
applyAuthorized helper that writes both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core change: both timers stop lying when the Mac sleeps or the user
walks away. Old code measured wallclock from a stored "start"
timestamp, so 4 hours of lid-closed showed up as 4 hours of "sitting"
and a 25-minute focus could quietly stretch across multiple hours
because Timer.scheduledTimer pauses during sleep.
What changed:
- Sit timer now reads CGEventSource HID idle to count only seconds
with real input activity. New Sources/engine/SystemIdle.swift
iterates the user-input event types and takes the min. Idle ≥ 5
min resets the accumulator (counts as a real break). Tick-gap >
30 s flags a sleep/wake and clears too — covers the case where
the user unlocks and idle resets to ~0 instantly.
- Pomodoro switched from "decrement counter every second" to a
wallclock end-time. Persisted to UserDefaults so quit/relaunch
recovers correctly: still-running phases keep counting; phases
whose end-time has already passed get caught up (one focus
credited if missed) and the timer returns to idle.
- Auto-loop with classic 4-pack rhythm: focus → short break →
focus → … on the 4th focus a long break (default 15 min) replaces
the short break, then the cycle resets. Default ON; toggle pill
in the settings row turns it off when the user wants manual
restarts.
- New cycle-progress dot row (●●○○) next to the phase badge so
the user can see where they are in the 4-pack at a glance.
- SitView tip text rewritten to reflect the new semantics: "只统计
你真正在键盘前的时间。离开 5 分钟以上自动归零,午休/会议不会被
算进去。"
User-facing wins:
- Closing the lid for lunch no longer fakes 90 minutes of sitting.
- A 25-min focus that survives a sleep cycle ends accurately.
- Start a focus once, the rhythm runs all morning.
Office-survival toolkit. Five tabs in the notch:
- 🍅 番茄钟: 25/5 focus/break with circular ring timer + tally.
- 🪑 久坐: count-up since last break, dock-bounce alert at 45min.
- 💧 喝水: animated cup, daily goal tracking with progress dots.
- 🏃 下班: H:M:S countdown to clock-out hour with gradient bar.
- 🎉 周末: 天/时/分 to next Saturday + rotating 打工语录.
100% local. UNUserNotificationCenter for alerts, dock-bounce
fallback when authorization denied. UserDefaults persistence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>