docs: mark M10 done

ARCHITECTURE.md's package table gets the new domain/backup,
domain/selfcheck, domain/widget, data/backup, backup/, widget/,
ui/settings/ and ui/widget/ entries, and §9/§10 are brought up to date
with what M10 actually built (the exported-component count, the
permission list unchanged, which 'not built yet' rows are now done).
CHANGELOG and ROADMAP's current-state line follow.
This commit is contained in:
2026-10-01 22:34:07 +02:00
parent fb81bfbf2e
commit 5d03c654bf
3 changed files with 56 additions and 29 deletions
+17
View File
@@ -181,6 +181,23 @@ Tag sections feed the release notes — see [`docs/RELEASING.md`](docs/RELEASING
- An alarm or timer set by voice without showing you a screen removes itself - An alarm or timer set by voice without showing you a screen removes itself
once it has been dismissed, so talking to your assistant every morning no once it has been dismissed, so talking to your assistant every morning no
longer leaves a list of dead 06:30 alarms behind. longer leaves a list of dead 06:30 alarms behind.
- A settings screen, reachable from a gear icon on every tab: theme, dynamic
colour and language, the alarm/timer/clock defaults every new alarm or timer
inherits, help, and the app version.
- Back up your alarms, timers and world clocks to a JSON file in a folder you
choose, and restore them on this device or another one. Restoring replaces
everything currently on the device — the app says so before you confirm.
Nothing about a ringing alarm or a running timer's live state travels with
the backup, only the configuration.
- A "why might my alarm not ring?" screen that checks the actual state of your
device — exact-alarm and notification permissions, full-screen alerts,
battery optimisation, Do Not Disturb, your alarm volume, and whether the
system has actually scheduled Clockula's next alarm — and links straight to
whichever settings page would fix each one.
- Three home-screen widgets: your next alarm, a running or paused timer (with
pause, resume and reset right on the widget), and a clock that grows to show
your world clocks at larger sizes. Each one updates the moment something
changes — no polling, no stale numbers.
### Changed ### Changed
- Tapping the alarm icon in the status bar, or the next-alarm line on the lock - Tapping the alarm icon in the status bar, or the next-alarm line on the lock
+25 -14
View File
@@ -96,6 +96,9 @@ kit module — the kit's roadmap keeps schedulers app-local.
| `domain/text/` | `TextFolding` (M9) — NFD-fold, strip combining marks, lowercase through `Locale.ROOT`, `containsFolded`. One copy, shared by the zone search and the label search | | `domain/text/` | `TextFolding` (M9) — NFD-fold, strip combining marks, lowercase through `Locale.ROOT`, `containsFolded`. One copy, shared by the zone search and the label search |
| `domain/live/` | `LivePillSelector` and its state — which running thing the live pill is about, as a pure function | | `domain/live/` | `LivePillSelector` and its state — which running thing the live pill is about, as a pure function |
| `domain/format/` | `ClockFormat` — `M:SS` / `H:MM:SS`, countdowns rounded up, elapsed truncated — `NextFire` (`NextFireLabel` + `NextFireFormat`), the alarm row's "in 9h 12m" as data, and `StopwatchFormat` — the hundredths readout, split into a major field and two digits so the fraction can be drawn smaller | | `domain/format/` | `ClockFormat` — `M:SS` / `H:MM:SS`, countdowns rounded up, elapsed truncated — `NextFire` (`NextFireLabel` + `NextFireFormat`), the alarm row's "in 9h 12m" as data, and `StopwatchFormat` — the hundredths readout, split into a major field and two digits so the fraction can be drawn smaller |
| `domain/backup/` | `BackupContent` (M10) — the plain Kotlin model between `data/backup`'s JSON codec and the repository/engine; decoupled from the wire DTOs the same way the data layer is decoupled from Room |
| `domain/selfcheck/` | `SelfCheck` (M10) — the "why might my alarm not ring?" rule table: pure functions over a `DeviceState` snapshot, each producing a `CheckResult` with a status and an optional deep-link `SelfCheckFix` |
| `domain/widget/` | the three widget presenters (M10) — `NextAlarmWidgetModel`/`TimerWidgetModel`/`ClockWidgetModel`: selection and formatting only, so a widget's content is unit-tested without Glance |
| `data/db/` | `ClockulaDatabase` — `@Database` v3, `exportSchema = true` — and `Migrations` | | `data/db/` | `ClockulaDatabase` — `@Database` v3, `exportSchema = true` — and `Migrations` |
| `data/alarms/` | the alarm and ring-state entities, DAOs, mappers and repositories | | `data/alarms/` | the alarm and ring-state entities, DAOs, mappers and repositories |
| `data/timers/` | `TimerEntity`, `TimerDao`, `TimerMapper`, `TimerRepository(+Impl)`, `TimerRingStateStore` (the ring session's one DataStore record) | | `data/timers/` | `TimerEntity`, `TimerDao`, `TimerMapper`, `TimerRepository(+Impl)`, `TimerRingStateStore` (the ring session's one DataStore record) |
@@ -105,7 +108,8 @@ kit module — the kit's roadmap keeps schedulers app-local.
| `data/prefs/` | `SettingsPrefs`, `ClockPrefs`, `StopwatchPrefs`, `TimerPrefs`, `UiPrefs` | | `data/prefs/` | `SettingsPrefs`, `ClockPrefs`, `StopwatchPrefs`, `TimerPrefs`, `UiPrefs` |
| `data/ringtones/` | the two ringtone seams — `RingtoneCatalog` (the device's alarm sounds, titles, playability, a SAF grant) and `RingtonePreviewer` — plus their `System*` implementations | | `data/ringtones/` | the two ringtone seams — `RingtoneCatalog` (the device's alarm sounds, titles, playability, a SAF grant) and `RingtonePreviewer` — plus their `System*` implementations |
| `data/time/` | `SystemWallClock`, `SystemElapsedRealtimeClock`, `SystemZoneProvider`, `AndroidBootIdProvider`, `RealTicker` | | `data/time/` | `SystemWallClock`, `SystemElapsedRealtimeClock`, `SystemZoneProvider`, `AndroidBootIdProvider`, `RealTicker` |
| `data/di/` | `DataModule`, `DatabaseModule`, `RepositoryModule`, `TimeModule`, `RingtoneModule`, `ZoneModule` (M8 — one `@Binds`, `ZoneNames → IcuZoneNames`) | | `data/di/` | `DataModule`, `DatabaseModule`, `RepositoryModule`, `TimeModule`, `RingtoneModule`, `ZoneModule` (M8 — one `@Binds`, `ZoneNames → IcuZoneNames`), `BackupModule` (M10) |
| `data/backup/` | the backup's data-layer half (M10): `BackupDto`/`BackupCodec` (the pure JSON wire format, `ignoreUnknownKeys`, strict on known fields), `BackupDao` (one `@Transaction` replace-all over alarms/timers/world clocks — no schema or version change), `BackupRepository(+Impl)` — mapping through the existing `AlarmMapper`/`TimerMapper`/`WorldClockMapper`, never a second copy |
| `alarm/` | `AlarmEngine` and the four seams it talks to — `AlarmScheduler`, `AlarmCapabilities`, `RingCoordinator`, `AlarmNotifier` — plus `AlarmIntents` | | `alarm/` | `AlarmEngine` and the four seams it talks to — `AlarmScheduler`, `AlarmCapabilities`, `RingCoordinator`, `AlarmNotifier` — plus `AlarmIntents` |
| `alarm/android/` | the seams' Android implementations: AlarmManager, the capability reads, the service handle, the snoozed notification | | `alarm/android/` | the seams' Android implementations: AlarmManager, the capability reads, the service handle, the snoozed notification |
| `alarm/receiver/` | `AlarmFireReceiver`, `AlarmActionReceiver`, `SystemEventReceiver` | | `alarm/receiver/` | `AlarmFireReceiver`, `AlarmActionReceiver`, `SystemEventReceiver` |
@@ -124,6 +128,8 @@ kit module — the kit's roadmap keeps schedulers app-local.
| `stopwatch/di/` | `StopwatchModule` — `@Binds` for the one seam | | `stopwatch/di/` | `StopwatchModule` — `@Binds` for the one seam |
| `interop/` | the `AlarmClock` boundary's Android half (M9): `AlarmClockActivity` (the one exported, permission-guarded, window-less door), `IntentExtrasReader` (`Bundle` → `IntentExtras`), `AlarmClockHandler` (the orchestrator — **no `android.*` import, ever**), `InteropIntents` (the internal transport to `MainActivity`) and `InteropLaunch` | | `interop/` | the `AlarmClock` boundary's Android half (M9): `AlarmClockActivity` (the one exported, permission-guarded, window-less door), `IntentExtrasReader` (`Bundle` → `IntentExtras`), `AlarmClockHandler` (the orchestrator — **no `android.*` import, ever**), `InteropIntents` (the internal transport to `MainActivity`) and `InteropLaunch` |
| `system/` | `RebootRepair` — the boot-id gate | | `system/` | `RebootRepair` — the boot-id gate |
| `backup/` | `BackupEngine` (M10) — the orchestrator: a ringing alarm dismissed and running timers deleted before a replace, `AlarmEngine.reschedule()`/`TimerEngine.resync()` after it — plus `BackupFiles`, the SAF seam, with its Android implementation in `backup/android/` |
| `widget/` | the three `GlanceAppWidget`/`GlanceAppWidgetReceiver` pairs (M10) and `WidgetSync` — the event-driven collector, started from `ClockulaApp.onCreate`, that redraws all three on any change to the state they mirror. `widget/di/` holds the `@EntryPoint` Glance needs, since a `GlanceAppWidgetReceiver` is never `@AndroidEntryPoint`-constructed |
| `ui/theme/`, `ui/crash/` | the M0/M1 theme and the crash-report surface | | `ui/theme/`, `ui/crash/` | the M0/M1 theme and the crash-report surface |
| `ui/shell/` | the navigation policy (`ShellNavigation`), the adaptive shell, the live pill and its source/ViewModel, the notification-permission ask. `EmptyTabScreen` is **gone**: its KDoc said it existed only until each of M5–M8 replaced its own tab, and the world clock was its last caller | | `ui/shell/` | the navigation policy (`ShellNavigation`), the adaptive shell, the live pill and its source/ViewModel, the notification-permission ask. `EmptyTabScreen` is **gone**: its KDoc said it existed only until each of M5–M8 replaced its own tab, and the world clock was its last caller |
| `ui/common/` | `rememberLocalTimeFormatter` — the app's **one** 12/24-hour formatter, over a `java.time.LocalTime`, with `rememberAlarmTimeFormatter` delegating to it (M8 D17) — and, since M6, the ringtone picker (`RingtonePickerState`, `RingtonePickerScreen`), which both editors need | | `ui/common/` | `rememberLocalTimeFormatter` — the app's **one** 12/24-hour formatter, over a `java.time.LocalTime`, with `rememberAlarmTimeFormatter` delegating to it (M8 D17) — and, since M6, the ringtone picker (`RingtonePickerState`, `RingtonePickerScreen`), which both editors need |
@@ -132,6 +138,8 @@ kit module — the kit's roadmap keeps schedulers app-local.
| `ui/stopwatch/` | the real Stopwatch tab (M7): the pure row builder (`StopwatchRows`) and its source, the ViewModel, the readout panel with its controls, the lap table and its column header, `StopwatchDefaults` | | `ui/stopwatch/` | the real Stopwatch tab (M7): the pure row builder (`StopwatchRows`) and its source, the ViewModel, the readout panel with its controls, the lap table and its column header, `StopwatchDefaults` |
| `ui/worldclock/` | the real World clock tab (M8): the pure row builders (`WorldClockRows`, `ZonePickerRows`) and their state, `WorldClockSource`, the ViewModel, the tab, the analog face, the row and the zone picker, `WorldClockDefaults` | | `ui/worldclock/` | the real World clock tab (M8): the pure row builders (`WorldClockRows`, `ZonePickerRows`) and their state, `WorldClockSource`, the ViewModel, the tab, the analog face, the row and the zone picker, `WorldClockDefaults` |
| `ui/ring/` | `AlarmRingActivity`, its ViewModel, `RingScreen` and the dismiss-challenge controls | | `ui/ring/` | `AlarmRingActivity`, its ViewModel, `RingScreen` and the dismiss-challenge controls |
| `ui/settings/` | the settings hub (M10): `SettingsScreen`/`SettingsViewModel` (appearance, alarm/timer/clock defaults, reliability, data, help), `SelfCheckScreen`/`SelfCheckViewModel`, `BackupScreen`/`BackupViewModel` — all reachable from a gear icon on every tab, with no owning tab of their own |
| `ui/widget/` | the three widgets' `@Composable` Glance content (M10) — the only Compose this milestone adds outside a tab; the `GlanceAppWidget`/`GlanceAppWidgetReceiver` classes themselves live in `widget/`, since they are manifest components, not UI |
--- ---
@@ -1025,11 +1033,17 @@ nothing flashes on the way through.
Components: `MainActivity`, the exported and permission-guarded Components: `MainActivity`, the exported and permission-guarded
`interop.AlarmClockActivity`, the non-exported `CrashReportActivity` and `interop.AlarmClockActivity`, the non-exported `CrashReportActivity` and
`AlarmRingActivity`, two foreground services (`AlarmRingService` and M6's `AlarmRingActivity`, three foreground services (`AlarmRingService`, M6's
`TimerService`), five receivers — `AlarmFireReceiver`, `AlarmActionReceiver`, `TimerService`, M7's `StopwatchService`), five non-widget receivers —
`SystemEventReceiver`, `TimerExpiryReceiver`, `TimerActionReceiver` — and `AlarmFireReceiver`, `AlarmActionReceiver`, `SystemEventReceiver`,
AppCompat's locale metadata holder service. Exactly **two** of them are `TimerExpiryReceiver`, `TimerActionReceiver` — AppCompat's locale metadata
exported, and `ManifestRulesTest` fails the build on a third. holder service, and, since M10, three `GlanceAppWidgetReceiver`s
(`NextAlarmWidgetReceiver`, `TimerWidgetReceiver`, `ClockWidgetReceiver`).
`ManifestRulesTest` pins the exported set exactly: the two above, plus the
three widget receivers — each with **only** the platform-required
`APPWIDGET_UPDATE` filter and no permission, since `APPWIDGET_UPDATE` is a
platform-delivered broadcast no other app can trigger by naming the action.
M10 adds no `<uses-permission>`.
--- ---
@@ -1037,22 +1051,19 @@ exported, and `ManifestRulesTest` fails the build on a third.
| Not here | Milestone | | Not here | Milestone |
|---|---| |---|---|
| The exact-alarm and full-screen-intent grant deep links, and any explanation of a denial. M4 asks for `POST_NOTIFICATIONS` once on first launch; the rest waits for the self-check screen, because a bare jump into system settings with no reason given is hostile | M10 |
| Reordering alarms by hand. The list is ordered by time of day (§13) and `alarms` has no `sort_order` column, so this would be a schema change for a preference the time order already answers | not planned for v1 | | Reordering alarms by hand. The list is ordered by time of day (§13) and `alarms` has no `sort_order` column, so this would be a schema change for a preference the time order already answers | not planned for v1 |
| A per-alarm auto-silence duration. Every other per-alarm setting became an override picker in M5; this one is still global and still `AlarmRing.AUTO_SILENCE_AFTER` | M10 at the earliest | | A per-alarm auto-silence duration. Every other per-alarm setting became an override picker in M5; this one is still global and still `AlarmRing.AUTO_SILENCE_AFTER` | not planned for v1 |
| Voice-interaction follow-on flows: `Activity.isVoiceInteraction`, `VoiceInteractor.CompleteVoiceRequest` (publishing a deeplink to the alarm just created) and `PickOptionRequest` (disambiguating by voice). `android.intent.category.VOICE` is therefore not declared — advertising it while answering with a touch dialog would be a promise the app does not keep. Clockula *accepts* a `clockula://` deeplink and publishes none | not planned for v1 | | Voice-interaction follow-on flows: `Activity.isVoiceInteraction`, `VoiceInteractor.CompleteVoiceRequest` (publishing a deeplink to the alarm just created) and `PickOptionRequest` (disambiguating by voice). `android.intent.category.VOICE` is therefore not declared — advertising it while answering with a touch dialog would be a promise the app does not keep. Clockula *accepts* a `clockula://` deeplink and publishes none | not planned for v1 |
| A "most closely matched" time search for `DISMISS_ALARM`. `ALARM_SEARCH_MODE_TIME` matches **exactly**: dismissing an alarm the caller did not name is a missed alarm, and there is no distance at which that becomes a good trade | not planned for v1 | | A "most closely matched" time search for `DISMISS_ALARM`. `ALARM_SEARCH_MODE_TIME` matches **exactly**: dismissing an alarm the caller did not name is a missed alarm, and there is no distance at which that becomes a good trade | not planned for v1 |
| A toast or any other confirmation on a `SKIP_UI` write. `SKIP_UI` means "bypass any intermediate UI", a voice assistant speaks its own confirmation, and the status-bar alarm icon `setAlarmClock` lights is the platform's own receipt (§17) | not planned for v1 | | A toast or any other confirmation on a `SKIP_UI` write. `SKIP_UI` means "bypass any intermediate UI", a voice assistant speaks its own confirmation, and the status-bar alarm icon `setAlarmClock` lights is the platform's own receipt (§17) | not planned for v1 |
| Surfacing `delete_after_use` in either editor. It is a property the contract sets, not a setting; an alarm the user edits keeps it, and that consequence is documented (§17) rather than papered over with a checkbox | not planned for v1 | | Surfacing `delete_after_use` in either editor. It is a property the contract sets, not a setting; an alarm the user edits keeps it, and that consequence is documented (§17) rather than papered over with a checkbox | not planned for v1 |
| Carrying `EXTRA_MESSAGE` into the timer setup panel when no length was given. There is no row to carry it on and the panel has no label field | not planned for v1 | | Carrying `EXTRA_MESSAGE` into the timer setup panel when no length was given. There is no row to carry it on and the panel has no label field | not planned for v1 |
| Verifying a ringtone URI at the intent boundary. M5 locked "an unreadable sound is reported, never rewritten"; an unmounted card comes back, and the boundary opens no `ContentResolver` | not planned for v1 | | Verifying a ringtone URI at the intent boundary. M5 locked "an unreadable sound is reported, never rewritten"; an unmounted card comes back, and the boundary opens no `ContentResolver` | not planned for v1 |
| Exporting `delete_after_use` in a backup. There is no backup yet; M10's JSON schema must include the column | M10 | | A user-configurable auto-silence duration, an unlimited-snooze option, per-alarm auto-silence | not planned for v1 |
| The self-check screen ("why might my alarm not ring?"), settings screen, JSON backup / SAF export | M10 |
| A user-configurable auto-silence duration, an unlimited-snooze option, per-alarm auto-silence | M10 at the earliest |
| Reordering timers by hand. `sort_order` and `TimerRepository.reorder` exist and stay caller-less: the list is in storage order, M5 gave the same answer for alarms, and a drag surface would also mean abandoning the `LazyColumn` | not planned for v1 | | Reordering timers by hand. `sort_order` and `TimerRepository.reorder` exist and stay caller-less: the list is in storage order, M5 gave the same answer for alarms, and a drag surface would also mean abandoning the `LazyColumn` | not planned for v1 |
| Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | M10 at the earliest | | Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | not planned for v1 |
| A "stop all" for several expired timers, and undo after a timer delete. Each expired timer is acknowledged on its own, and the delete confirmation asks before rather than after | not planned for v1 | | A "stop all" for several expired timers, and undo after a timer delete. Each expired timer is acknowledged on its own, and the delete confirmation asks before rather than after | not planned for v1 |
| A presets management screen, and editing `default_timer_duration_millis` or `default_timer_ringtone_uri`. M6 reads both and offers the two-gesture preset surface on the setup panel | M10 | | A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_duration_millis` and `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 |
| A timer that ramps its volume, a dismiss challenge or a snooze for a timer | not planned for v1 | | A timer that ramps its volume, a dismiss challenge or a snooze for a timer | not planned for v1 |
| Surfacing `TimerSnapshot.anchorIsStale` in the UI. The boot repair makes it vanishingly rare and the value is already honest; a row does not say "estimated" in v1 | not planned for v1 | | Surfacing `TimerSnapshot.anchorIsStale` in the UI. The boot repair makes it vanishingly rare and the value is already honest; a row does not say "estimated" in v1 | not planned for v1 |
| Lap export, sharing, deletion or editing, and any "previous run" history. A run's laps belong to the run that produced them; keeping old runs is a different feature with its own table | not planned for v1 | | Lap export, sharing, deletion or editing, and any "previous run" history. A run's laps belong to the run that produced them; keeping old runs is a different feature with its own table | not planned for v1 |
@@ -1062,7 +1073,7 @@ exported, and `ManifestRulesTest` fails the build on a third.
| A countdown-to-start, split-lap alarms, or any sound or vibration from the stopwatch. It is silent, and a build rule says so (§8) | not planned for v1 | | A countdown-to-start, split-lap alarms, or any sound or vibration from the stopwatch. It is silent, and a build rule says so (§8) | not planned for v1 |
| An analog stopwatch face or a `MaterialShapes` showpiece on the Stopwatch tab. `PLAN.md` §8 sanctions the showpiece for the analog world-clock face, the ring screen and timer progress; a stopwatch has no progress to be a fraction of | not planned for v1 | | An analog stopwatch face or a `MaterialShapes` showpiece on the Stopwatch tab. `PLAN.md` §8 sanctions the showpiece for the analog world-clock face, the ring screen and timer progress; a stopwatch has no progress to be a fraction of | not planned for v1 |
| Surfacing `StopwatchReading.anchorIsStale` in the UI, for the same reason the timers' is not: the repair makes it momentary, and the value displayed is already honest | not planned for v1 | | Surfacing `StopwatchReading.anchorIsStale` in the UI, for the same reason the timers' is not: the repair makes it momentary, and the value displayed is already honest | not planned for v1 |
| A rename UI for `world_clocks.label`. The column is honoured on **read** — a row shows the label when it has one — but M8 adds no way to write it; M10's backup import will carry labels | M10 | | A rename UI for `world_clocks.label`. The column is honoured on **read** — a row shows the label when it has one — and M10's backup now carries it on export/import, but there is still no way to *write* a label in-app | not planned for v1 |
| An automatic "home" row while travelling. Google Clock's automatic home clock is a separate feature with its own preference and its own edge cases; M8's answer to "where am I" is the hero face, which already follows the device when no home zone is stored | not planned for v1 | | An automatic "home" row while travelling. Google Clock's automatic home clock is a separate feature with its own preference and its own edge cases; M8's answer to "where am I" is the hero face, which already follows the device when no home zone is stored | not planned for v1 |
| A per-city detail screen, a map, sunrise/sunset times or a day-length bar. `DayNight` is a stated convention for a shape morph, not an ephemeris (§16) | not planned for v1 | | A per-city detail screen, a map, sunrise/sunset times or a day-length bar. `DayNight` is a stated convention for a shape morph, not an ephemeris (§16) | not planned for v1 |
| Live times or long zone names in the zone picker. 450 rows of ICU long names is real work for a list the user scrolls past, and a ticking picker is a ticking picker | not planned for v1 | | Live times or long zone names in the zone picker. 450 rows of ICU long names is real work for a list the user scrolls past, and a ticking picker is a ticking picker | not planned for v1 |
+14 -15
View File
@@ -10,20 +10,19 @@ Status legend: ✅ done · 🚧 in progress · ⬜ not started
## Current state (one line) ## Current state (one line)
✅ **M9 done.** Every app on the device — an assistant, an automation app, a ✅ **M10 done.** Clockula now has a settings hub (theme, dynamic colour,
watch companion, a shell script — can now drive Clockula through language, the alarm/timer/clock defaults every new alarm or timer inherits,
`android.provider.AlarmClock`, and no extra it sends can make Clockula write help), a documented, versioned JSON backup (`docs/BACKUP.md`) that exports and
something the user did not ask for. All **seven** of the contract's actions are restores alarms/timers/world clocks and settings through SAF — configuration
answered through **one** exported, permission-guarded, window-less door, behind only, never ring state, never a timer's running anchors — as a whole-state
which everything that decides is pure Kotlin: an hour of 25 is dropped rather replace the screen is explicit about before the user confirms, a "why might my
than clamped to 23, a snooze of 1 000 minutes is clamped rather than refused, a alarm not ring?" self-check that reads real device state (exact-alarm and
`file://` ringtone inherits the default instead of blocking the alarm, and an notification permissions, full-screen intent, battery optimisation, DND, alarm
intent hostile in every extra at once still leaves exactly one alarm and the volume, a known aggressive-OEM allowlist, and the system's own next alarm
user looking at it. A voice-set alarm is **transient** — the schema went to v3 compared against what Clockula computed) and deep-links to the exact settings
for it — so "wake me at 6:30" every morning no longer leaves a graveyard of dead page for each, and three Glance home-screen widgets — next alarm, timer,
06:30 rows. And the status-bar alarm icon finally opens the alarms list instead clock — that mirror existing state and redraw event-driven, on every write,
of a ring screen for an alarm that is not ringing. **M10** (settings, JSON with no ticker of their own. **M11** (release readiness) is next.
backup and the self-check screen) is next.
--- ---
@@ -586,7 +585,7 @@ next-alarm publishing. Hostile-input validation at the intent boundary.
hardware**, as no device was attached — the same caveat M5's seven, M6's four, hardware**, as no device was attached — the same caveat M5's seven, M6's four,
M7's four and M8's four carry. M7's four and M8's four carry.
### ⬜ M10 — Settings, backup, the self-check, and widgets ### ✅ M10 — Settings, backup, the self-check, and widgets
- Settings composed from kit components: theme, dynamic colour, language, - Settings composed from kit components: theme, dynamic colour, language,
alarm/timer/clock defaults, about + crash reporting. alarm/timer/clock defaults, about + crash reporting.
- JSON backup export/import via SAF (`PLAN.md` §7), schema documented in - JSON backup export/import via SAF (`PLAN.md` §7), schema documented in