OmniPlanner Devlog
Building a Life OS
Local-first planning, calendar AI, habit tracking, and the plan-vs-actual gap
I. Before the Repository
It started as a web app. Vite, React, a weekly planner with tabs for goals and habits. The usual stuff. But the data lived in localStorage inside a browser tab, and the whole thing disappeared if you cleared your cache. That felt wrong for something meant to hold your life plan.
The question was: what if a planner was a real app? Something you could trust with years of goals, habits, and history. Something that never sends your data to a server. Something that works on your machine, on your terms.
Electron was the fastest path. Tauri was considered, but Electron's ecosystem for IMAP, notifications, and cross-platform builds was more mature. The first commit converted the web app into a desktop app in a single day.
The first commits were not glamorous. electron-main.cjs, preload.cjs, run.js launcher, electron-builder config targeting Windows NSIS, macOS DMG, and Linux AppImage. AGPL-3.0 license. GitHub Actions CI that builds executables on every push.
Same day: Gemini AI provider wired in. Dead code removed. The project had a protected boundary.
II. The Week Is an Island
The core architectural bet: week-isolated planning. Each week is an independent page. Goals, todos, habits, calendar events — all scoped to that week. Changing this week doesn't affect last week or next week. Nothing bleeds.
This sounds simple. It's not. Most planners treat time as a flat list. OmniPlanner treats each week as a bounded context with its own goals, its own daily plans, its own habit tracker. The WeekData type became the atom of the system.
Phase 2 introduced GoalItem: structured life goals at 10Y, 5Y, 3Y, 1Y, monthly, and weekly horizons. Legacy text blobs were migrated into typed records. Weekly goals could link upward to life goals via Todo.parentGoalId.
Phase 4 hardened credentials. services/storage/secureSettings.ts became the single boundary for API keys and IMAP passwords. Electron.safeStorage encrypted everything at rest.
The storage layer was abstracted behind a StorageAdapter interface. LocalStorageAdapter for Electron, IndexedDBAdapter for web/PWA. Swappable without touching domain logic. Schema migrations ran automatically on startup.
III. Calendar as Source of Truth
A planner without a calendar is a todo list. A calendar without a planner is a schedule. OmniPlanner needed both, connected.
Phase 5 added CalendarEventKind: meetings, focus blocks, task blocks, and routines. Each could link to a goal and a todo. The weekly grid showed time-blocked events as colored bands.
Phase 6 added execution analytics: the weekly review sidebar showed focus block count, goals with time support, completed linked tasks, and unscheduled work that still needed blocks.
Email integration came next. IMAP connection with Gmail, Outlook, Yahoo, Naver. Emails could be read inline next to calendar events. One-click "Create Event from Email" parsed meeting requests into calendar blocks.
Email errors got a full taxonomy: EMAIL_AUTH_FAILED, EMAIL_IMAP_DISABLED, EMAIL_APP_PASSWORD_REQUIRED, EMAIL_NETWORK_TIMEOUT. Each with a calm, user-facing message and an operation ID for correlating logs. No raw credential leakage in error messages.
IV. Hardening
138 tests across 7 files. Backup validation. Migration idempotency. Onboarding detection. The app had to survive real use — corrupt storage, failed imports, users who clear their cache.
Phase 12 added conflict-free restore: preview backup metadata before overwriting, handle legacy formats, survive mid-migration crashes.
Phase 19 was the cleanup pass: strict typecheck with zero errors, ID normalization from number to string, component extraction, AI API cleanup.
Phase 22 added OAuth token lifecycle: safe refresh, re-auth on expiry, IMAP preserved as fallback.
V. The Main Screen Problem
The weekly planner was solid. Habits worked. Calendar connected to email. But the main screen was a grid — and nobody opens a planner to see a grid. They open it to know what's next.
The planner was complete. The entrance was wrong.
So the main screen became a Dashboard. Today's top events ranked by priority × urgency. Habits due now with one-tap check-off. Recent emails. Top todos. Quick-add forms for new todos and new habits.
Every event card has a Start button. Tap it when the event begins — the app logs your actual start time. Skip marks it missed. Snooze reminds you in 5 minutes. Sleep events get "Going to bed" instead of "Start".
Priority stars (1-5) on every event and todo. Manual priority always overrides the default heuristic. Urgency (due soonest) fills in when you haven't set priority. The score is priority × 20 + urgency.
All mutations flow through setAllWeeks → Weekly Planner, Monthly View, and Goals View all see the same data. No sync needed because there's one source of truth.
VI. Pulse — The Alarm System
A calendar event at 11pm should auto-create a wind-down alarm at 10pm and a wake alarm at 6am. A meeting at 2pm should auto-create a prep alarm at 1:30pm. Not manually. Automatically.
The alarm rules engine derives alarms from calendar events. Each rule has an event kind filter, an offset in minutes (negative = before, positive = after), and a template string. The calendar is the single source of truth for alarms.
The Pulse tab shows: daily reminder toggles (morning planner, habit check-in, focus block alert), upcoming event-derived alarms, and sleep alarm rules. Master enable/disable at the top.
Mobile notifications fire via @capacitor/local-notifications. Desktop notifications are the next piece.
138 tests still pass. Typecheck clean. Electron 42 (upgraded from 26). Auto-backup on app exit. Window state persistence. GitHub release version checking. Corrupt storage auto-repair.
VII. What's Next
The plan-vs-actual overlay on the weekly grid. Soft translucent blocks for what you planned. Solid opaque blocks for what actually happened. Per-day toggle to compare.
Desktop notifications via Electron's Notification API. Rich alerts with Start/Skip/Snooze actions baked into the notification itself.
Wearable and lock screen widgets. "What's next?" on your wrist without opening the app.
AI ranking: when an AI provider is configured, the dashboard ranking learns from your patterns. "You always skip gym on Mondays. Want to reschedule?"
Covered Commits
This devlog covers the full OmniPlanner build from February to May 2026.
0534b6cFixed infini load time/ clean up2bc3be3Correct OmniPlanner license notice0434ebdAdd AGPL licensee0867c2Phase 21-C: OAuth token lifecycle3bec6fcPhase 21-B: OAuth login foundation6ea2c38Phase 22: email reliability and diagnostics451faeaPhase 21: test coverage and release confidence24634e4Phase 20: reminder visibility polish326dcf0Phase 19: engineering hardening96ceb4dPhase 13: Mobile UX and daily-use polish301a9d4Phase 12: Offline resilience and conflict-free restore890faaePhase 11: Native secure storage and reminder wiring20c5d2cPhase 10: Capacitor mobile shell and notification service1fa7885Phase 9: web shell and local-first PWA945e4c3Phase 6: execution analytics and weekly reviewe435576Phase 5: calendar execution and planning intelligence3e79bbePhase 3: goal-to-week linking via Todo.parentGoalId62577c8Phase 2: GoalItem domain model and migration v22485fafPhase 0+1: product docs and storage abstractiondaf14d3Add multi-AI provider system (Gemini, Claude, OpenAI)4a74631Convert from web app to local Electron desktop app725ced4Fix upload, Electron crash, AI service, and build issues
Next: the plan-vs-actual weekly overlay, desktop notifications, and AI-powered ranking.