CalEmdar — plan

Written 2026-10-20, before any code. Built autonomously at Jonathan's request. Every decision below was made without asking; the reasons are written next to each one so they can be revisited.

1. What it is

An ad-free, shared calendar for Emily (and Jonathan). It's the third app in the hirota.us "Em" family, after Emify (music.hirota.us) and QuizEm (quiz.hirota.us).

2. Research: what people hate about calendar apps, and what CalEmdar does about it

Sources: Apple Community and MacRumors threads, App Store and G2 reviews of Fantastical, Cozi and TimeTree reviews, iOS 26 complaint coverage (Yahoo Tech, MacMagazine), the HEY Calendar wishlist, unstar.app's 2026 calendar-app roundup, and iCloud calendar-spam coverage (BleepingComputer, iMore).

#Complaint (who)CalEmdar's answer
1Month view shows dots, not titles (Fantastical's top complaint)Month cells show real event titles. On iPhone, tapping a day lists its events under the grid, like the old iOS "month + list" layout that iOS 26 users miss.
2Natural-language entry is weak (Apple) or paywalled (Fantastical)A free quick-add bar that understands typed sentences, for example "Lunch with Sam Fri 1pm at Hilo Bay Cafe", "Yoga every Tue and Thu 6pm" or "Mom's birthday Mar 5 every year". A live preview shows what it understood before you save, so a misread time is caught.
3No undo; a deleted event is gone (iPhone Calendar has no undo; restoring means rolling back all of iCloud)Every create, edit, move and delete shows an Undo toast (also ⌘Z on the Mac). Deleted events go to Recently Deleted for 30 days.
4"This and all future events" is confusing, and editing one repeat sometimes duplicates the series (Outlook, Google)Editing or deleting a repeating event asks in plain words: "Only this one (Tue, Oct 21)", "This and later ones", or "Every one". Each choice is stored as a proper exception or split. Nothing is ever duplicated, and every choice can be undone.
5Repeat options are too narrow, e.g. no "second Tuesday of every month" (Google)Repeats cover daily, weekly on chosen days, monthly by date or by "2nd Tuesday" / "last Friday", and yearly. Each has an interval and an end (never, on a date, or after N times).
6Reminders fail silently (Google after iOS updates; push permissions reset)Server-sent Web Push reminders, with more than one alert per event and default alerts per calendar. Settings has a "Send a test" button and shows whether notifications are working on this device. A reminder missed during a short Pi outage is still sent within a small window.
7Overlapping events are unreadable or untappableDay and week views pack overlapping events side by side in columns. On iPhone, a crowded slot collapses into a "+N more" chip that opens a list.
8Time zones: all-day events shift a day when traveling; "3pm Pacific" lands at local time (Apple, Fantastical)All-day events are stored as plain dates and never shift. Timed events keep their time zone, so a weekly 9am meeting stays at 9am across daylight-saving changes. Quick-add understands "Pacific", "HST" and similar. An optional second time zone column covers the Hawaii ↔ mainland case.
9Sync is slow between devices (Apple, Outlook)The Pi is the single source of truth. Changes show instantly on the device that made them. Other devices refresh when the app comes to the front and every 30 s while it's open.
10Two people edit the same event and one edit is lost (shared calendars)Every event carries a version number. If it changed on another device in the meantime, the app says so and offers "Keep mine" or "Use theirs". It never silently overwrites.
11Ads and surprise paywalls (Cozi audio ads, Fantastical moving features to a subscription)No ads and no tiers. It's ours.
12Widgets don't show today or tomorrow (Apple)A PWA can't make iOS widgets. Instead, CalEmdar publishes a private subscribe link: added to Apple Calendar, its events appear in the native widgets, on Apple Watch, in Siri and in CarPlay. The app itself opens on an "Up next" summary.
13Spam calendar invites (iCloud)There's no invite inbox, so there's no spam.
14The app ignores the default view on launch (iOS 26 bug)It reopens on the last view, or always on Today if that setting is on.
15No week numbers, weekends can't be hidden, the week always starts on SundayAll three are settings.
16No year view that shows only the big things (HEY wishlist)A Year view with 12 mini-months, plus a list of starred events and birthdays.
17Retyping the location for repeated events; no copy/duplicateLocations and titles autocomplete from past events. Choosing a past title also fills in its calendar, length and location. There's a Duplicate action.
18Dragging events is clumsy (Google)Long-press and drag to move or resize on touch; plain drag with the mouse. Snaps to 15 minutes, and every drag can be undone.
19Tiny text in month view; days mashed together (Google mobile)Larger type, a text-size setting, and a month grid that drops to compact chips before it ever becomes illegible.
20Search is broken or slow (iOS 26)Instant search across titles, locations and notes, past and future, including repeating events.
21Not enough colors (Google)16 accessible calendar colors, plus an optional per-event color. Color is never the only signal: every calendar also has a name and a mark.
22Birthdays are just eventsBirthday events can store a birth year, and then show the age ("Mom turns 60").
23Overlaps go unnoticed when bookingThe editor warns "Overlaps with Dentist (2:00–3:00)" before you save.

3. Scope

v1 (this build):

Deliberately left out of v1, and why:

4. Architecture

iPhone / Mac (installed PWA) ──HTTPS──> Cloudflare ──> NPM (cal.hirota.us) ──> calemdar:3000 (Express)
                                                                                    │
                                                                         MySQL `calemdar` DB (own user)
                                                                                    │
                                              Web Push (Apple/FCM) <── reminder loop (every 30 s)
                                              outside ICS feeds    <── subscription refresher (SSRF-guarded)

Data model (MySQL)

API (JSON; every mutating request needs Content-Type: application/json and a same-origin Origin)

5. UX design

Look: the hirota.us family, with Emify/QuizEm pink as the accent.

iPhone layout:

Mac layout (≥ 900 px):

States, on every view:

Settings:

6. Security (an app that knows where people will be)

7. Testing and verification

8. Deployment

9. Build order

  1. Scaffold the repo, server skeleton, DB and auth (copied from QuizEm's proven pieces).
  2. Write the core modules test-first: tz, rrule, occurrences, layout, nlp, ics, holidays.
  3. Build the API and DB, with integration tests.
  4. Build the UI shell (tokens, layout, router, store/sync) and the Month, Day/3-Day/Week, List and Year views.
  5. Add the editor, quick add, recurrence scopes, undo/trash, drag, duplicate and the conflict warning.
  6. Add reminders/push, the subscribe feed, outside subscriptions, holidays, import/export and settings.
  7. Polish: accessibility, keyboard, offline, a WebKit visual QA loop, light/dark.
  8. Infra and deploy: verify the served bytes, write memory notes, and send Jonathan the summary.

Progress is tracked in docs/STATUS.md.