# Calendula — Roadmap

## v0.x — Pre-Release

| Version | Milestone | Status |
|---|---|---|
| v0.1 | Foundation & CI | complete |
| v0.2 | Data Layer & Permission Flow | complete |
| v0.3 | Month + Week + Day views, view switcher | complete |
| v0.4 | Event Detail (S4) + humanized recurrence | complete |
| v0.5 | Calendar filter (M3) + Settings (M4) | complete |
| v0.6 | Full event read — surface every readable field | complete |
| v1.0 | First public release — polish pass, F-Droid | complete |

Delivery ran ahead of the original table: Day view (S3) shipped in v0.3 and
Event Detail (S4) in v0.4, so the Filter/Settings milestone became v0.5.

Jump-to-date (the date-picker half of M2) was **cut from scope** and will not
ship. The "Today" half of M2 already shipped in v0.5 (drawer entry).

## v0.6 — Full event read

Round out the read-only model so a detail view shows everything the system
actually stores, before write support starts. Scope = `CalendarContract`
columns we don't yet read/display:

- **Reminders** (`VALARM`) — read `CalendarContract.Reminders`, list lead times
- **Status** — Confirmed / Tentative / Cancelled (cancelled shown struck-through)
- **Availability** (`TRANSP`) — Free / Busy chip
- **Attendee extras** — role (required / optional / organizer) + the user's own
  `SELF_ATTENDEE_STATUS`
- **Timezone** (`EVENT_TIMEZONE`) — shown only when it differs from the device zone
- **URL** — ~~tappable link card~~ **cut**: `CalendarContract` exposes no
  `Events.URL` column (only `CUSTOM_APP_URI`, an originating-app deep-link).
  URLs are instead surfaced by linkifying the description text
- **Access level / class** (private / confidential) — small chip (optional, trivial)

All of the above shipped in v0.6.0 (2026-06-11).

Deliberately out of v0.6:
- Recurrence exception / modified-occurrence badges — `Instances` already
  resolves correct per-occurrence times for display; this only matters for
  editing, so it folds into v2
- `CATEGORIES`, `ATTACH` — not reliably exposed by `CalendarContract`
  (provider limitation, not our choice)

## v1.0 — First Public Release — shipped 2026-06-11

All V1 features shipped, polished, on F-Droid. Read-only calendar. Cut directly
after v0.6 (full event read) plus the onboarding-screen polish pass.

### Polish backlog (pre-1.0)
- ~~Redesign the initial grant-access (permission) screen~~ — **done**
  (Material 3 Expressive onboarding, shipped in v0.6.0 / v1.0.0)

## v2.0 — Write Support (complete, shipped 2026-06-11)

Delivered in four releasable slices (plan:
`docs/superpowers/plans/2026-06-11-03-write-support.md`). The V1 spec is a
guide here, not a contract — scope per slice is decided as we go.

| Version | Milestone | Status |
|---|---|---|
| v1.1 | Write foundation — `WRITE_CALENDAR`, read-only-calendar detection, delete (series + single occurrence) | complete (shipped 2026-06-11) |
| v1.2 | Create event — form, FAB, last-used-calendar preselect | complete (shipped 2026-06-11) |
| v1.2.1 | Form polish after on-device review — card design system, optional fields + settings defaults, OptionCard dialogs, expressive motion | complete (shipped 2026-06-11) |
| v1.3 | Edit event — shared form, scoped recurring writes (this / following / all), recurrence picker | complete (shipped 2026-06-11) |
| v1.4 | Reminder notifications — see below | complete (shipped 2026-06-11) |
| v2.0 | Conflict dialog, polish pass (store copy refresh, F-Droid screenshots), release | complete (shipped 2026-06-11) |

v2.0 scope was re-cut on 2026-06-11, after v1.4:
- **Occurrence edit** already shipped early, in v1.3.
- **Quick-add** is **cut from scope**: the full form already opens prefilled
  (visible day, last-used calendar, optional fields hidden), so the sheet
  would only save one screen transition while adding a second create-surface
  to maintain. Revisit only if real-world feedback says creation feels heavy.
- **Calendar switching while editing** moves to the v3 backlog (sync-adapter
  minefield: `CALENDAR_ID` is sync-adapter-owned, AOSP locks the field; an
  honest implementation is copy+delete like Google Calendar, with sync-identity
  and attendee side effects).
- **Conflict dialog** stays (plan 03, decision 5): on save, compare against
  the row as it was when the form loaded; on external change, ask
  overwrite / discard. Closes the silent-clobber gap on synced calendars.

## v1.4 — Reminder Notifications

**Essential**, not nice-to-have: Calendula targets users for whom it is their
*only* calendar app, so reminder delivery can't be delegated to Google/OEM
Calendar. The calendar provider schedules reminders and broadcasts
`android.intent.action.EVENT_REMINDER`, but it does **not** post the visible
notification — a calendar app must. We become that app (the Etar model).

Scope:
- Manifest-registered `BroadcastReceiver` for `EVENT_REMINDER`
  (data scheme `content://com.android.calendar`) — wakes us at reminder time,
  no foreground service.
- Read `CalendarContract.CalendarAlerts` / `Reminders`, filter to
  `METHOD_ALERT` / `METHOD_DEFAULT` (skip `METHOD_EMAIL`); post on a dedicated
  notification channel; tap opens event detail.
- `POST_NOTIFICATIONS` runtime permission (API 33+) — requested in onboarding.
- Onboarding step: (a) request `POST_NOTIFICATIONS`, (b) in-app reminders
  toggle, **default ON**, with copy warning that a second calendar app with
  notifications on will cause duplicate reminders. Mirrored into Settings
  (reversible).

Deliberately deferred (add only if needed):
- Snooze / dismiss notification actions (Etar has them)
- Battery-optimization exemption prompt for delivery reliability

## v2.1 — Month event grid + drawer view tabs (shipped 2026-06-15)

- Month grid shows real events as continuous multi-day bars (not just dots)
- View section in the navigation drawer to switch Month / Week / Day
- Fix: text cursor no longer jumps in event text fields

## v2.2 — Tap-to-create + local calendar management (shipped 2026-06-16)

- Tap an empty slot in day/week → create form prefilled with that day + the
  tapped hour (snapped to the hour, 1 h long)
- Local (device-only) calendar management in a full-screen editor from
  Settings → Calendars: create / rename / recolor / delete, with name,
  pastel-previewed colour, and description (stored in `CAL_SYNC1`)
- Synced calendars listed read-only, grouped by account, each with a
  per-account "manage in source app" deep-link (resolved from the account's
  authenticator — DAVx5/ICSx5/…) + an add-account shortcut
- Shared `InlineTextField` extracted to `ui.common` (event form + calendar
  editor share one input style)

## v2.3 — Material 3 grouped-list redesign (shipped 2026-06-16)

A structural + visual pass adopting one shared blueprint (modelled on the ReFra
gallery app) across Settings, the calendar manager and the navigation drawer.

- Shared `ui/common/GroupedList.kt`: `CollapsingScaffold` (a `LargeTopAppBar`
  whose title collapses on scroll) + `GroupedRow` (Position-based corner
  grouping, press-animated corners, `selected` + `minHeight` knobs).
- Settings: category hub with About card on top and sliding sub-pages
  (Appearance / New event form / Notifications); theme/week-start/language
  pickers moved from `DropdownMenu` to OptionCard dialogs; token-based icon
  chips; `ic_gitea.xml` for the About "Source" button.
- Calendar manager + drawer restyled to match; shared `CalendarColorChip`;
  drawer scrolls as one with the active view highlighted.
- Cards use `surfaceContainerHigh` for readable contrast.
- Donate button on the About card deferred (target TBD).

---

# Backlog (theme-based, post-v2.1)

The old v3.0 / "daily-driver polish" / "Locations & People" lists are
consolidated here by theme. Within a group, **(in progress)** /
**(next)** mark what is being or about to be worked; everything else is an
approved-but-unscheduled idea unless tagged **(idea)** /
**(go/no-go)** / **(rejected)**. Order across groups is not a commitment.

## Near-term sequence (ranked, 2026-06-16)

The theme groups below are the full menu; this is the committed *order* for
the next stretch. Ranking favours finishing the current create/edit + calendar
arc before opening new fronts, then cheap-relative-to-value items and ones that
unblock a later item. Order is a plan, not a contract — revisit after each lands.

**Tier 1 — finish the current arc (create/edit + calendars)**
1. Tap-to-create in day/week *(shipped v2.2.0)* — prefilled create from an empty slot
2. Local calendar management + "manage in source app" deep-links *(shipped v2.2.0)*
3. ~~Settings redesign & restructure~~ *(shipped v2.3.0 — grew into the full
   grouped-list blueprint across Settings + calendars + drawer; see "v2.3"
   above)*
4. ~~Per-event color~~ *(shipped v2.4.0)* — palette calendars write
   `EVENT_COLOR_KEY` (sync-safe); local/opted-in calendars write a raw
   `EVENT_COLOR`; off-by-default setting for no-palette synced calendars
Tier 1's create/edit + calendars arc is effectively closed. **Duplicate event**
was deprioritised (2026-06-17) as low-importance and dropped to the bottom of
the sequence; the next item is now **Jump-to-date** (formerly Tier 2).

(Tier 2+ numbering below shifts accordingly; ranking unchanged.)

### Settings redesign & restructure *(shipped v2.3.0)*

The original scope below is kept as a record; the implementation expanded from a
sub-screen restructure into the shared grouped-list blueprint (see "v2.3" above).

The settings screen has grown into a flat vertical scroll of divider-separated
sections (Appearance, Event form, Notifications, Calendars, Language, About) and
will keep accreting rows (per-event-color defaults, default reminder, more
calendar entries are all queued). It needs structure before it gets unwieldy.

**Decided (2026-06-16): sub-screens**, not flat-but-carded. The top level
becomes a category list; each category opens its own destination. More
M3-idiomatic for a settings surface that will keep growing, and it mirrors the
existing Calendars row, which already navigates out to its own screen.

Structure — top-level settings list → category destinations:
- **Appearance** → theme, dynamic colour, week start
- **Event form** → the 6 default-field toggles + the hint text
- **Notifications** → reminders toggle (POST_NOTIFICATIONS flow stays)
- **Calendars** → already its own screen (`CalendarsScreen`); just becomes a
  peer category row, no change to that screen
- **Language** → single control; keep as a top-level row that opens an
  OptionCard directly (a whole sub-screen for one choice is overkill)
- **About** → kept inline on the top-level list as a card (read-only info,
  not worth a navigation hop). Card layout, top → bottom:
  - **Identity** — app logo + name "Calendula", with "by Jean-Luc Makiola"
    as a subtitle beneath the name
  - **Action buttons** (small, button-styled, sit in a row):
    - **Source** — Gitea logo, opens the repo (`about_source_url`)
    - **License** — opens the LICENSE file on Gitea
    - **Donate** *(tentative)* — sits next to Source; target TBD (decide
      before building: Liberapay / Ko-fi / Gitea sponsor / etc.)
  - **Version** — small version number at the bottom of the card

Scope:
- **Navigation** — add the settings sub-screen destinations alongside the
  existing settings/calendars routes in `CalendarHost`; back pops to the
  settings list (mind the existing `BackHandler` that guards against falling
  through to the activity).
- **Fix the dialog-pattern violation** — theme, week-start and language use
  `DropdownMenu`; the project default is the full-width tonal OptionCard modal
  (radio/dropdown/text-list dialogs are banned, see
  `option-card-modal-style-default`). Migrate these selectors to OptionCard.
- **Visual pass** — top-level category rows with leading icons; consistent
  spacing and row affordances aligned with the event-form card design system.

Out of scope (no new settings *features* here) — this is a structure + style
pass on the existing controls; new toggles ride in with their own features.

**Tier 2 — navigation & daily-driver completeness**
5. ~~Jump-to-date — drawer date picker (un-cut from V1); cheap, fills the nav gap~~ *(done, v2.5.0)*
6. ~~Agenda view — the missing 4th view; serves daily-driver users *and* becomes the data source for the widget~~ *(done, v2.5.0)*

**Tier 3 — platform reach (depends on Tier 2)**
7. ~~Home-screen widget — built on the agenda data source from #6~~ *(done, v2.5.0 — agenda + month widgets)*
8. App shortcuts: ~~launcher long-press → New event~~ *(done, v2.5.0)*; ~~quick-settings tile~~ *(done, v2.8.0 — "New event" QS tile)*

**Tier 4 — reliability, data-safety & interop** *(re-ranked 2026-06-17)*
9. **Reminders — defaults + delivery reliability** *(shipped v2.6.0)* — global
   default reminder **+ per-calendar override**, bundled with battery-exemption
   hardening. Full sketch in "Reminders — defaults & delivery reliability" below.
10. **The `.ics` engine — export + import** *(shipped v2.7.0, 2026-06-18)* — one
    hand-rolled serializer/parser (zero deps, stays on `kotlinx-datetime`),
    four surfaces all shipped: single-event share + whole-calendar backup
    (export), open-`.ics`→form + whole-calendar bulk import (import). Closed the
    device-local-calendar data-loss gap (#10/#11 merged here). Built as two
    sequential branches: `feat/ics-export` (write side + UID-on-create precursor)
    then `feat/ics-import` (parser, restore, dedup by UID). Import is
    liberal-in/strict-out: skip-and-report foreign `VTIMEZONE` / `RECURRENCE-ID`
    / guest lists it can't model. Plans:
    `docs/superpowers/plans/2026-06-18-05-ics-export.md` + `…-06-ics-import.md`.
11. **Snooze / dismiss notification actions** *(merged into release/v2.8.0)* —
    followed the `.ics` work; inherits v2.6's deferred exact-alarm/WorkManager
    decision (snooze must re-fire an alarm).

**Tier 5 — close the read/write gap on the event model** *(opened 2026-06-22)*
12. **Attendee editing** *(shipped — merged into release/v2.8.0, `feat/attendee-editing`
    MR !29 / commit `b0f34ff`, 2026-06-22)* — closed the last big read-only gap in
    the event model: attendees were already *read* (queried, mapped, shown on the
    detail screen since v0.6) and are now *writable* from the form. An attendees
    section on `EventEditScreen` / `EventForm` adds by typed email **or** the
    no-permission contact picker, edits/removes rows, and sets role (required /
    optional) — writing `CalendarContract.Attendees` rows on insert + dirty-checked
    update, mirroring the reminders-diff pattern. The sync-adapter *invitation*
    caveat was **settled record-only** (Calendula never sends invites; the backend
    decides delivery) — full sketch + decision in "Attendee editing" under Locations
    & People below.

**Gated — explicit go/no-go before any work (mostly INTERNET-permission calls)**
- Remote calendar create/edit (re-implements DAVx5; INTERNET + credential storage)
- Locations & People — the no-permission contact pickers already shipped (location v2.8.0, attendee email v2.8.0); what remains gated is OSM autocomplete (needs INTERNET)
- Move event to another calendar — sync-adapter minefield (copy+delete model)

**Bottom — deprioritised, not important**
- Duplicate event (detail action → prefilled create form) — moved here
  2026-06-17; cheap but low value, pick up only if asked

**Unranked / fill-in** — pinch-to-zoom time scale, tablet/foldable layouts.
Pulled in opportunistically, not sequenced.

Tier 4 is now fully shipped (#9 reminders defaults v2.6.0, #10 `.ics`
export/import v2.7.0, #11 snooze/dismiss in release/v2.8.0; drag-drop rejected),
and Tier 5 #12 — attendee editing — shipped in release/v2.8.0, closing the last
read-only gap in the event model. v2.8.0 also cleared most Tier 2/3 leftovers —
full-text search, the "New event" Quick Settings tile, and the now-line all
shipped there. **No tier work is currently committed as next;** the remaining
candidates are unscheduled theme-group ideas (pinch-to-zoom, tablet/foldable,
accessibility pass) plus the gated go/no-go items.

## Navigation & views

- ~~Tap an empty slot in day/week → create form prefilled with that
  date+time, snapped to the hour~~ **shipped v2.2.0** (long-press variant
  not added — single tap covers it)
- Agenda view (fourth view: upcoming events grouped by day; also the
  natural data source for a future widget)
- Jump to date — drawer date picker (un-cut from V1)
- ~~Current-time "now" line in day/week~~ **shipped v2.8.0**
- Week numbers in the **month** grid — **rejected** (owner decision): clutters
  the view and shrinks the day cells; the badge stays week-view-only.
- Pinch-to-zoom time scale in day/week
- Tablet / foldable layouts *(was v3.0)*
- ~~Full-text search~~ **shipped v2.8.0** — for a daily driver with real event
  history, finding an event is core completeness, not optional.

## Event editing & creation

- Duplicate event (detail action → prefilled create form)
- ~~**Per-event color**~~ *(shipped v2.4.0)* — `EVENT_COLOR` / `EVENT_COLOR_KEY`
  from the calendar's color list (`Colors` table, `TYPE_EVENT`), OptionCard
  picker in the form, falling back to the calendar color when unset. Reused the
  color-picker component and palette plumbing from local calendar management and
  finished the create/edit theme.

## Calendars & accounts

- ~~Create / manage local (device-only) calendars~~ **shipped v2.2.0** —
  name + color + description; rename / recolor / delete the calendars the app
  owns. Inserted under `ACCOUNT_TYPE_LOCAL` as a sync adapter; description in
  `CAL_SYNC1`. Full-screen "Calendars" editor reached from Settings.
- ~~Per-calendar "manage in source app" deep-link~~ **shipped v2.2.0** — for
  synced calendars, open the app the calendar actually came from based on
  its `ACCOUNT_TYPE` (DAVx5 `bitfire.at.davdroid`, Google `com.google`,
  …); fall back to system account/sync settings. Plus an "add account"
  entry into system Accounts. Honest boundary for remote calendars.
- **Remote calendar create/edit** *(go/no-go)* — creating a CalDAV
  collection (`MKCALENDAR`) or a Google calendar means an in-app sync
  client: **INTERNET permission, credential storage, the full server
  round-trip** — i.e. re-implementing DAVx5. DAVx5 exposes no public
  intent to delegate the create to it. Cosmetic local edits (color/name)
  to an existing synced row are possible but don't propagate to the server
  and may be overwritten on next sync — not promised. Same explicit
  go/no-go gate as the OSM/INTERNET item below.
- Move event to another calendar (copy+delete model with a consequences
  warning — deferred from v2.0; `CALENDAR_ID` is sync-adapter-owned) *(was v3.0)*
- ~~**Local-calendar backup / export** *(Tier 4 #10)*~~ **shipped v2.7.0** —
  device-only (`ACCOUNT_TYPE_LOCAL`) calendars had no sync and therefore no
  backup. Settings → Calendars → Export writes every event to a user-chosen
  `.ics` file (SAF); restore is the bulk-import path (pick a calendar, dedup by
  UID). Closed the silent data-loss gap.

### Disable a calendar in-app *(captured 2026-06-25)*

A second, heavier visibility level **above** the existing per-view filter. Today
the drawer's calendar filter (`hiddenCalendarIds` in `CalendarPrefs`) only hides
a calendar's *events* from the month/week/day/agenda views — the calendar itself
still clutters the drawer filter list, the event-form calendar picker, and the
import target picker. "Disable" removes a calendar from the app's surfaces
entirely; "hide" stays the lightweight, frequently-toggled control.

**Two-level model (both kept):**
- **Hidden** (existing) — `hiddenCalendarIds`; a quick per-view checkbox in the
  drawer. Toggles events on/off in the views; the calendar stays listed
  everywhere. Operates only over the *enabled* calendars.
- **Disabled** (new) — the calendar is gone from the app: not in the drawer
  filter list, not in the event-form picker, not in the import picker, and its
  events never appear (it's gated out of `instances()` like a hidden one). It
  remains visible **only** in Settings → Calendars, where the enable/disable
  toggle lives, so it can be brought back.

**Storage — app-side (DataStore), mirrors the hidden set.** Add
`disabledCalendarIds: Set<Long>` + `setDisabledCalendarIds(...)` to
`CalendarPrefs` (comma-separated string key, same shape as `hiddenCalendarIds`).
**Does not touch** `CalendarContract.Calendars.VISIBLE` / `SYNC_EVENTS` — purely
a Calendula-local preference, so other calendar apps are unaffected and the sync
adapters stay out of it (privacy-clean, reversible).

**Where the disabled set is applied:**
- `CalendarRepositoryImpl.instances()` + `searchEvents()` — exclude
  `calendarId ∈ (hidden ∪ disabled)` so a disabled calendar's events never show
  and aren't searchable. (`repository.calendars()` itself stays unfiltered/raw —
  the screens that need everything still get everything.)
- `FilterViewModel.state` — drop disabled calendars from the drawer filter list
  (you can't hide/show what's disabled).
- `EventEditViewModel.writableCalendars` — exclude disabled, so you can't create
  into a calendar you've removed from the app. Handle the last-used-calendar
  preselect falling on a now-disabled calendar (fall back to first enabled
  writable).
- `ImportViewModel` — exclude disabled from the import target list.
- `CalendarsScreen` / `CalendarsViewModel` — the **only** surface that lists
  disabled calendars; add `setDisabled(id, Boolean)` and a per-calendar toggle.

**UI — Settings → Calendars (no new tab).** Add an enable/disable control to each
row on the existing `CalendarsScreen` (both the local and the synced/read-only
groups — disabling is an app-side view choice, independent of write access).
Disabled rows render visibly de-emphasised (dimmed) but keep the toggle so
they're re-enableable. Follow the project dialog/list conventions (M3 grouped
list, `option-card-modal-style-default` if a confirm/selection surface is needed).

**Decided behaviour:**
- All calendars disabled → views show the existing empty state.
- A disabled calendar that still holds events: events simply vanish from
  views/search until re-enabled (no data touched — it's a filter, not a delete).
- Deep links / notifications pointing at an event in a disabled calendar still
  open its detail screen — detail is a direct id lookup, not an `instances()`
  query, so disabling never strands an existing link.

## Reminders — defaults & delivery reliability *(shipped v2.6.0; built on `feat/default-reminders`)*

Two themes bundled because both are "make reminders trustworthy" — the core of
the "Calendula is your only calendar app" promise.

**Built in this slice (A + the safe half of B):** global timed default reminder
+ a **separate all-day default** (day-scale lead times) + per-calendar override
(timed events), applied on create with manual-edit / calendar-switch / all-day-
toggle handling; three pickers + per-calendar override list in Settings →
Notifications; battery-optimisation exemption row (status + system deep-link, no
extra permission). `resolveDefaultReminder` + prefs round-trips unit-tested.
Resolution model: all-day events use the all-day global default outright;
per-calendar overrides govern timed events only. Reviewed (8-angle), fixes
applied: form-reset state race, label-fn consolidation with the detail screen,
inline wrapper + single combined flow read.

**Deliberately deferred (documented decisions, not oversights):**
- *Absolute time-of-day for all-day reminders* — the all-day default is still
  minutes-before-midnight (day-scale presets), not "9am the day before" (open
  decision #2's richer half). Per-calendar all-day overrides also deferred.
- *Self-scheduled alarms* — kept the existing provider-broadcast architecture
  (open decision #1). The battery exemption is the reliability lever; no
  `AlarmManager`/`USE_EXACT_ALARM` subsystem was added.
- *Test-reminder diagnostic* and *battery prompt inside onboarding* — the
  exemption lives only in Settings for now (onboarding flow untouched to keep
  the change reviewable).

### A. Default reminders (global + per-calendar override)

**No provider backing.** `CalendarContract` has no column that auto-applies a
default reminder per calendar — Google's per-calendar defaults live server-side.
So both the global default *and* the per-calendar override are **app-side
preferences**, applied by us at event-insert time. We inherit nothing from the
synced calendar.

- **Storage (DataStore):**
  - `defaultReminderMinutes: Int?` — global default; `null` = "no reminder".
  - `defaultAllDayReminderMinutes: Int?` — separate all-day default (all-day
    reminders are expressed as minutes before midnight / day-before-at-time, not
    minutes before a start instant — they need their own value).
  - `perCalendarReminderOverride: Map<Long, Int?>` — keyed by calendar id;
    **absent key = inherit global**, explicit `null` = "no reminder for this
    calendar". (Same for an all-day override map if we want per-calendar all-day.)
- **Apply on create:** a fresh event prefills its reminders list from
  override-or-global for the preselected calendar. Changing the calendar in the
  form re-applies the *new* calendar's default **only if the user hasn't manually
  edited the reminders** — track a dirty flag, mirroring the per-event-color
  reset pattern (v2.4).
- **Edit semantics:** defaults apply to **new events only**; never rewrite
  reminders on existing events on open or on calendar-switch-during-edit.
- **Settings UI (Notifications sub-page):**
  - Global default via OptionCard (None / at time of event / 5 / 10 / 15 / 30 min
    / 1 h / 1 day / custom), plus the separate all-day default.
  - Per-calendar overrides: a row per writable calendar (in the Calendars screen
    or a Notifications subsection), each opening the same OptionCard with a
    leading **"Use global default"** option.

### B. Delivery reliability (exact alarms + battery)

The provider broadcasts `EVENT_REMINDER`, but on modern Android (Doze / OEM
battery managers) delivery can be silently delayed or dropped. v1.4 deferred this;
it directly undermines the feature's premise, so it rides in here.

- **Exact alarm — decision first:** trust the provider broadcast, or
  self-schedule via `AlarmManager.setExactAndAllowWhileIdle` for reliability?
  If we self-schedule, declare `USE_EXACT_ALARM` (API 33+, auto-granted for
  calendar/alarm-category apps, F-Droid-clean) with a `SCHEDULE_EXACT_ALARM`
  fallback for API 31–32 (user-revocable → settings deep-link prompt).
- **Battery-optimization exemption:** a *soft, optional* prompt via
  `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` (settings deep-link — never the
  auto-grant intent), honest copy: "Android may delay reminders to save battery;
  exempt Calendula for on-time delivery." Shown once after the existing
  `POST_NOTIFICATIONS` onboarding step, reversible in Settings → Notifications.
- **Diagnostics:** a "send a test reminder in 1 minute" button in Notifications
  settings so users can verify delivery on their specific OEM (Samsung / Xiaomi
  are notorious for suppressing it).

### Decisions made (as shipped in v2.6.0)

1. **Provider broadcast kept** — did not self-schedule via `AlarmManager`; the
   battery-optimisation exemption is the reliability lever (simplicity + battery
   cost won over the exact-alarm subsystem).
2. **All-day reminders = minutes-before-midnight** (day-scale presets); absolute
   time-of-day ("9am the day before") deferred — see the deferred list above.
3. **Per-calendar overrides live in the Notifications sub-page** (override list),
   governing timed events only.

### Round two

- ~~Snooze + dismiss actions on the notification~~ *(shipped in release/v2.8.0,
  Tier 4 #11)* — snooze re-fires a snooze-only exact alarm.

## Sharing & interop

- ~~Share event as .ics + open/receive .ics into a prefilled create form~~
  **shipped v2.7.0** — single-event share from detail; opening an `.ics` with one
  event prefills the create form, many events opens a bulk import (dedup by UID)
- ~~ICS file import~~ **shipped v2.7.0** — covered by the open/receive `.ics`
  flow above (single → form, many → bulk import)

## Platform & launchers

- ~~Home-screen widget~~ **shipped v2.5.0** — agenda + month widgets
- ~~App shortcuts (launcher long-press → New event)~~ **shipped v2.5.0**
- ~~"New event" Quick Settings tile~~ **shipped v2.8.0**

## Quality & reliability

- **Accessibility pass** — TalkBack content descriptions across all screens,
  dynamic-type / large-font reflow, touch-target audit. Quality bar for an
  F-Droid app; nothing tracks it yet.
- **Reminder delivery reliability** — exact alarms + battery-optimization
  exemption; specced in the "Reminders — defaults & delivery reliability" slice
  above (Tier 4 #9).

## Locations & People *(go/no-go, captured 2026-06-11)*

Beyond classic calendar-client scope; discussed, deliberately not planned
in detail yet:

- ~~**Contact address picker** for the location field via the system picker
  (`ACTION_PICK`)~~ **shipped v2.8.0** — one-shot, needs no READ_CONTACTS, fits
  the privacy story. The same no-permission mechanism was then reused for the
  attendee email picker (v2.8.0).
- **OSM address autocomplete** in the location field (type "Brandenburger
  Tor" → tap suggestion → resolved address inserted). Backend would be
  Photon (Nominatim's public policy forbids autocomplete). **Requires the
  INTERNET permission** — first dent in the "no network access" promise;
  if built: opt-in (off by default), honest copy, configurable endpoint
  for self-hosters, onboarding footnote + F-Droid copy reworded. This
  trade-off is an explicit go/no-go decision before any work starts.
- **Inline contact suggestions** while typing (needs READ_CONTACTS) — only
  if the picker proves clunky.
- **Attendee editing** *(promoted out of this gated bucket 2026-06-22 — now
  Tier 5 #12, high-importance; the no-permission typed-email path is not an
  INTERNET/contacts call)*. See the "Attendee editing" sketch below.

### Attendee editing *(Tier 5 #12, opened 2026-06-22)*

The last read-only gap in the event model: attendees are read & shown on the
detail screen (since v0.6) but the form can't write them. Make guests editable.

- **Read side already done:** `Attendee` domain model + status/relationship/type
  enums, `queryAttendees` + `EventDetailMapper.toAttendee` (with tests), and the
  attendees `DetailCard` + `AttendeeRow` in `EventDetailScreen`. Nothing to add
  there.
- **Write side — SHIPPED (`feat/attendee-editing`, commit b0f34ff, 2026-06-22):**
  `attendees` on `EventForm`; a Guests section in `EventEditScreen` rendered as an
  inline grouped list — each guest a tonal card (avatar, name/email, tappable
  Required/Optional role chip, remove), the trailing card an inline email field
  (type → Done commits). Persisted by `reconcileAttendees` diffing the provider's
  `CalendarContract.Attendees` rows on insert + dirty-checked update (new guests =
  `RELATIONSHIP_ATTENDEE` / `STATUS_INVITED`; kept rows keep their status, only the
  required/optional type updates; organizer/resource/no-email rows untouched).
  Reminders restyled to the same grouped-list pattern in passing. Needs only the
  existing `WRITE_CALENDAR` — no new permission.
- **Name on manual add — SHIPPED via a contact picker** (2026-06-22). The inline
  field is email-only (the quick path); the add row also has a **"from contacts"
  button**: `ACTION_PICK` on `ContactsContract.CommonDataKinds.Email.CONTENT_URI` →
  the system Contacts app returns a URI to the picked email row, queried for
  `Email.ADDRESS` **and** the contact `DISPLAY_NAME`, so a picked guest gets both
  email and name in one tap. The result Intent grants temporary read access, so
  **no `READ_CONTACTS` permission** — one-shot and user-driven, same no-permission
  mechanism as the location address picker above. Self/organizer rows stay
  non-editable.
- **Invitation behavior — DECIDED 2026-06-22: record-only, all writable
  calendars.** Calendula has no INTERNET and never sends an invitation itself; it
  only writes `Attendees` rows. Whether a guest is notified is decided downstream:
  local calendars notify no one (no sync); CalDAV/DAVx5 PUTs the `ATTENDEE` lines
  and the *server* decides iMIP delivery; Google's sync adapter pushes the change
  and Google decides (third-party attendee writes are historically unreliable
  there). Editing is allowed on **any writable calendar** — not gated to local.
  - **Honest, backend-aware copy is mandatory** (this is the whole point of the
    decision): on a synced calendar show "Calendula doesn't send invitations —
    your calendar account may email guests when it syncs"; on a local calendar
    show "Stored on this device. No one is notified."
  - Calendula must **not fabricate an ORGANIZER** or otherwise fake scheduling
    state to coax a send — it writes the guest list faithfully and leaves
    scheduling entirely to the backend.
  - The optional "send an .ics invite via your email app" delegate (`ACTION_SEND`,
    still no INTERNET) was considered and **deferred** — revisit only if users ask
    to notify guests explicitly.
- **Out of scope (for now):** RSVP/your-own-response editing, free/busy lookups,
  resource booking — all carry server round-trips or richer sync semantics.

## Consciously rejected

- Travel time / weather / smart suggestions (network, core-promise conflict)
- Natural-language quick entry (high effort, locale-fragile; the prefilled
  form already covers fast entry)
- Quick-add sheet (the prefilled full form already covers it — cut in v2.0)
- Drag & drop rescheduling in day/week — **rejected** (owner decision,
  reaffirmed 2026-06-22): not wanted. Rescheduling stays via the edit form.
