Show birthdays from the address book as a calendar #206

Closed
opened 2026-09-02 06:28:35 +00:00 by jcoffey-dev · 0 comments
Owner

The dates were already on the contact cards and nothing ever showed them — so the one thing a birthday is for, noticing it in time, was the one thing the app could not do with it.

Derived, not stored

The dates stay on the cards. A second copy of the same fact drifts the first time somebody corrects one, and keeping a calendar of its own is exactly what ihasmail does not do. Entries are generated when a view asks for a range and vanish when the contact does.

They go through instancesIn like everything else, so no view has to know they are different — month, week, day and agenda all got it for free.

Off until switched on

It is derived data, and a calendar that fills itself with dates nobody put there is a surprise rather than a feature. It can also be hidden from the calendar's own sidebar without being turned off, which is the same distinction the shared calendars already draw.

They cannot be edited, and that is not special-cased

The virtual calendar reports no write rights, so every control that already asks before offering Edit or Delete declines on its own — verified in the app, where the popover for a birthday shows only Close.

updateEvent and destroyEvent also refuse a synthesised id, so the store is safe whatever calls it, including anything added later. That matters for drag-to-reschedule (#15), which would otherwise try to save one.

Two conventions about the dates

A card with only a day and month — the common case, not the exceptional one — gets a birthday with no age, rather than no birthday.

29 February falls on the 28th in a year that has no 29th. Somebody born in February has a birthday in February; moving it into March is the arithmetic winning over the fact. Both are conventions, and these are the ones that keep the fact intact.

Testing

15 unit tests: the age and the no-year case, occurrences across a multi-year range, both leap-year behaviours, timestamp as well as partial dates, non-birth anniversaries ignored, the name falling back through components to the organisation, a future birth year not producing a negative age, impossible dates rejected, date ordering, stable unique ids that mark themselves as synthesised, and ranges that are empty, backwards or absurdly wide.

Suite green: web 732 across 80 files, server 110, typecheck clean.

Verified in the running app

The calendar appears in the sidebar and toggles; June 2026 shows "Grace Hopper's birthday" with no age (card has no year) and "Linus Torvalds's birthday (114)" from 1912; August shows one aged 108 from 1918; and the popover for a birthday offers only Close — no Edit, no Delete.

The mock now carries birthdays on most of its contacts, including one with no year and one on 29 February, so both cases are visible without a real address book.

Merged 2026-09-01 as coffey-labs/ihasmail@6c7c6d19b3

Rebuilt from: git history, session transcript.

The dates were already on the contact cards and nothing ever showed them — so the one thing a birthday is for, noticing it in time, was the one thing the app could not do with it. ## Derived, not stored The dates stay on the cards. A second copy of the same fact drifts the first time somebody corrects one, and keeping a calendar of its own is exactly what ihasmail does not do. Entries are generated when a view asks for a range and vanish when the contact does. They go through `instancesIn` like everything else, so no view has to know they are different — month, week, day and agenda all got it for free. ## Off until switched on It is derived data, and a calendar that fills itself with dates nobody put there is a surprise rather than a feature. It can also be hidden from the calendar's own sidebar *without* being turned off, which is the same distinction the shared calendars already draw. ## They cannot be edited, and that is not special-cased The virtual calendar reports no write rights, so every control that already asks before offering Edit or Delete declines on its own — verified in the app, where the popover for a birthday shows only Close. `updateEvent` and `destroyEvent` also refuse a synthesised id, so the store is safe whatever calls it, including anything added later. That matters for drag-to-reschedule (#15), which would otherwise try to save one. ## Two conventions about the dates **A card with only a day and month** — the common case, not the exceptional one — gets a birthday with **no age**, rather than no birthday. **29 February falls on the 28th** in a year that has no 29th. Somebody born in February has a birthday in February; moving it into March is the arithmetic winning over the fact. Both are conventions, and these are the ones that keep the fact intact. ## Testing 15 unit tests: the age and the no-year case, occurrences across a multi-year range, both leap-year behaviours, timestamp as well as partial dates, non-birth anniversaries ignored, the name falling back through components to the organisation, a future birth year not producing a negative age, impossible dates rejected, date ordering, stable unique ids that mark themselves as synthesised, and ranges that are empty, backwards or absurdly wide. Suite green: web 732 across 80 files, server 110, typecheck clean. ## Verified in the running app The calendar appears in the sidebar and toggles; June 2026 shows "Grace Hopper's birthday" with no age (card has no year) and "Linus Torvalds's birthday (114)" from 1912; August shows one aged 108 from 1918; and the popover for a birthday offers only Close — no Edit, no Delete. The mock now carries birthdays on most of its contacts, including one with no year and one on 29 February, so both cases are visible without a real address book. **Merged** 2026-09-01 as coffey-labs/ihasmail@6c7c6d19b3a1 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.