Fixes#25, including the addendum about the delete dialog.
Editing a one-time event showed "This is a recurring event — changes apply to the whole series", and deleting one asked "Delete all occurrences?" / "This will delete the entire series" for an event that has exactly one.
Three places treated the presence of baseEventId as proof of recurrence:
EventEditor.tsx — the info box
EventPopover.tsx — the delete confirmation
CalendarContextMenu.tsx — the same confirmation from the right-click menu
That is not what the field means. loadRange queries with expandRecurrences: true, and Stalwart sets baseEventId on everything it returns that way — a one-off event included, pointing at itself. The mock never sets the field at all, which is why nothing caught this until it met a real server.
All three now share isRecurring() in store/calendar.ts. It asks about recurrence rules, which are what actually make a series, and also treats a base that is a different event as an occurrence of one — so if an expanded instance ever arrives without its rules attached, the delete dialog still tells the truth about what it is deleting. Deletion behaviour is unchanged; only the wording was wrong.
One thing worth a glance against the live 0.16.19 when convenient: this assumes Stalwart gives a non-recurring event a baseEventId equal to its own id, rather than minting a synthetic instance id for it. That matches the reported symptom and the spec reading. If it turns out to mint one, the one-off case needs the rules check alone — the test file has both shapes written out.
Tested: npm run typecheck, npm test (184 web + 88 server, all passing), with a new store/__tests__/recurrence.test.ts covering a one-off with a self-referential base, a bare event, a series by rules, and an occurrence whose base is another event.
Fixes #25, including the addendum about the delete dialog.
Editing a one-time event showed "This is a recurring event — changes apply to the whole series", and deleting one asked "Delete all occurrences?" / "This will delete the entire series" for an event that has exactly one.
Three places treated the presence of `baseEventId` as proof of recurrence:
- `EventEditor.tsx` — the info box
- `EventPopover.tsx` — the delete confirmation
- `CalendarContextMenu.tsx` — the same confirmation from the right-click menu
That is not what the field means. `loadRange` queries with `expandRecurrences: true`, and Stalwart sets `baseEventId` on everything it returns that way — a one-off event included, pointing at itself. The mock never sets the field at all, which is why nothing caught this until it met a real server.
All three now share `isRecurring()` in `store/calendar.ts`. It asks about recurrence rules, which are what actually make a series, and also treats a base that is a *different* event as an occurrence of one — so if an expanded instance ever arrives without its rules attached, the delete dialog still tells the truth about what it is deleting. Deletion behaviour is unchanged; only the wording was wrong.
One thing worth a glance against the live 0.16.19 when convenient: this assumes Stalwart gives a non-recurring event a `baseEventId` equal to its own id, rather than minting a synthetic instance id for it. That matches the reported symptom and the spec reading. If it turns out to mint one, the one-off case needs the rules check alone — the test file has both shapes written out.
Tested: `npm run typecheck`, `npm test` (184 web + 88 server, all passing), with a new `store/__tests__/recurrence.test.ts` covering a one-off with a self-referential base, a bare event, a series by rules, and an occurrence whose base is another event.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
**Merged** 2026-08-25 as coffey-labs/ihasmail@9a7f37b5404f
<sub>Rebuilt from: git history, session transcript.</sub>
Checked against the live 0.16.19 rather than leaving it as an assumption — and the assumption was wrong.
A one-off event comes back from the expanded query as id: "eaaaaai", baseEventId: "i": Stalwart mints an instance id for it, and the base is a different id. So the "a base that is another event means it is an occurrence" clause would have kept calling every event a series, i.e. would not have fixed this at all on a real server.
Pushed 62e3b7a: recurrence rules alone decide it now, and the test carries the real id shape.
What that gives up is an expanded instance that arrives without its rules attached. The account I probed has a single non-recurring event, so that case is still unverified — it needs a real series to check, which means writing to a live calendar. Following that up separately.
Checked against the live 0.16.19 rather than leaving it as an assumption — and the assumption was wrong.
A one-off event comes back from the expanded query as `id: "eaaaaai"`, `baseEventId: "i"`: Stalwart mints an instance id for it, and the base is a *different* id. So the "a base that is another event means it is an occurrence" clause would have kept calling every event a series, i.e. would not have fixed this at all on a real server.
Pushed 62e3b7a: recurrence rules alone decide it now, and the test carries the real id shape.
What that gives up is an expanded instance that arrives without its rules attached. The account I probed has a single non-recurring event, so that case is still unverified — it needs a real series to check, which means writing to a live calendar. Following that up separately.
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #25, including the addendum about the delete dialog.
Editing a one-time event showed "This is a recurring event — changes apply to the whole series", and deleting one asked "Delete all occurrences?" / "This will delete the entire series" for an event that has exactly one.
Three places treated the presence of
baseEventIdas proof of recurrence:EventEditor.tsx— the info boxEventPopover.tsx— the delete confirmationCalendarContextMenu.tsx— the same confirmation from the right-click menuThat is not what the field means.
loadRangequeries withexpandRecurrences: true, and Stalwart setsbaseEventIdon everything it returns that way — a one-off event included, pointing at itself. The mock never sets the field at all, which is why nothing caught this until it met a real server.All three now share
isRecurring()instore/calendar.ts. It asks about recurrence rules, which are what actually make a series, and also treats a base that is a different event as an occurrence of one — so if an expanded instance ever arrives without its rules attached, the delete dialog still tells the truth about what it is deleting. Deletion behaviour is unchanged; only the wording was wrong.One thing worth a glance against the live 0.16.19 when convenient: this assumes Stalwart gives a non-recurring event a
baseEventIdequal to its own id, rather than minting a synthetic instance id for it. That matches the reported symptom and the spec reading. If it turns out to mint one, the one-off case needs the rules check alone — the test file has both shapes written out.Tested:
npm run typecheck,npm test(184 web + 88 server, all passing), with a newstore/__tests__/recurrence.test.tscovering a one-off with a self-referential base, a bare event, a series by rules, and an occurrence whose base is another event.🤖 Generated with Claude Code
Merged 2026-08-25 as coffey-labs/ihasmail@9a7f37b540
Rebuilt from: git history, session transcript.
Checked against the live 0.16.19 rather than leaving it as an assumption — and the assumption was wrong.
A one-off event comes back from the expanded query as
id: "eaaaaai",baseEventId: "i": Stalwart mints an instance id for it, and the base is a different id. So the "a base that is another event means it is an occurrence" clause would have kept calling every event a series, i.e. would not have fixed this at all on a real server.Pushed 62e3b7a: recurrence rules alone decide it now, and the test carries the real id shape.
What that gives up is an expanded instance that arrives without its rules attached. The account I probed has a single non-recurring event, so that case is still unverified — it needs a real series to check, which means writing to a live calendar. Following that up separately.