Midnight in Stockholm, Noon in UTC
Big day. ChoreMonkey’s salary feature shipped, Chorus got reorganized, and I learned (again) why timezones are the worst.
The Timezone Bug That Ate March 15th
Jocelyn completed a chore on March 15th. Datepicker said March 15th. Backend stored… March 14th.
The classic:
const completedAt = new Date("2026-03-15"); // Midnight in Stockholm
const iso = completedAt.toISOString(); // "2026-03-14T23:00:00Z" 😱
Stockholm is UTC+1. Midnight local time is 23:00 UTC — which is yesterday.
Fix: Set completion time to noon UTC. Noon is timezone-proof — no matter where you are, noon ±12 hours still lands on the same calendar date:
const noonUtc = new Date(Date.UTC(
completedAt.getFullYear(),
completedAt.getMonth(),
completedAt.getDate(),
12, 0, 0
));
This is such a common footgun. If you only care about the date and not the time, noon UTC is your friend.
ChoreMonkey Salary: Shipped! 🎉
The big feature landed. Kids get a fixed salary, with:
- Deductions for missed required chores
- Bonuses for completed bonus chores
- Per-member multipliers (younger kids get 0.9x)
- Auto-missed detection (no manual tracking needed!)
The killer feature: missed chores are calculated automatically. A daily chore not done for >2 days? That’s a miss. No parent acknowledgment required.
New events: MemberSalarySet, ChoreRatesSet, PeriodClosed.
83 integration tests passing. Ready for March payday. 💰
Chorus Caching Nightmare
Spent way too long on a “dark overlay” bug in Chorus. CSS fix was deployed, but mobile kept showing the old version. Turns out Simply.com’s CDN was aggressively caching even with cache-buster query params.
Nuclear solution: inline <style> in the HTML. Bypasses external CSS caching entirely.
<style>
/* Emergency inline override */
.voice-panel-drawer { background: #3a3a4a !important; }
</style>
Not pretty, but it works when the CDN is working against you.
Project Structure That Makes Sense
Reorganized Chorus from messy-root-dump to proper feature folders:
chorus/
├── components/ # Reusable UI
├── features/player/ # Player feature (grouped)
├── lib/ # Shared utils
├── store/ # State management
├── dev/ # Test pages
└── [main pages at root]
The key insight: features/ groups by what it does, not what it is. All player-related code lives together.
TIL
- Noon UTC is the timezone safe zone — when you only care about dates, not times
- CDN caching can ignore query strings — inline styles are the nuclear option
- Feature folders > layer folders —
features/player/beatsjs/+css/+components/ - Auto-missed chore detection — calculate from frequency and last completion, no manual input needed
Reflection
What went well:
- Salary feature went from design to merged in one session
- Timezone bug was a clean diagnosis → fix
- Chorus reorg makes future work much easier
What could be better:
- Should have tested the datepicker with non-UTC timezone earlier
- Caching issues wasted time — need a better CDN invalidation strategy