docs(openspec): add SDD artifact trail for recent alarm and EQ changes
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s

Persist the exploration, proposal, spec, design, tasks, and
verify/archive reports produced during the multi-device EQ,
alarm-countdown, and notification-visual-polish SDD cycles.
This commit is contained in:
2026-07-04 12:42:11 +02:00
parent e5b6d8acb3
commit bccc5c48b8
64 changed files with 6020 additions and 0 deletions
@@ -0,0 +1,104 @@
# Alarm Pre-Notice L10n Specification
## Purpose
Define the required behavior for the pre-notice notification text when an alarm is
approaching. The notification MUST display computed remaining minutes in the device
locale rather than hardcoded Spanish text.
## Requirements
### Requirement: Computed Remaining Minutes
The system MUST compute the number of minutes remaining until alarm fire time at the
moment the pre-notice `BroadcastReceiver` fires and MUST use that value as the
displayed countdown, NOT a hardcoded string.
Remaining minutes MUST be floored (integer division). If the computed value is less
than 1 minute, the system MUST clamp to 1 and display "1 min".
#### Scenario: Normal pre-notice (alarm is ~30 min away)
- GIVEN an alarm is scheduled 30 minutes in the future
- WHEN the pre-notice `BroadcastReceiver` fires at `triggerAtMillis - PRE_NOTICE_MILLIS`
- THEN the notification body displays the remaining minutes derived from
`(triggerAtMillis - currentTimeMillis) / 60_000`
- AND the displayed value is a positive integer (e.g. "30 min")
#### Scenario: Alarm scheduled with less than 30 min remaining
- GIVEN an alarm is scheduled with fewer than 30 minutes from now
- WHEN the pre-notice fires immediately (or is already past)
- THEN the computed remaining minutes MAY be 0 or negative
- AND the system clamps the displayed value to a minimum of 1 minute
#### Scenario: System clock drift
- GIVEN the device clock drifts so `currentTimeMillis` exceeds `triggerAtMillis`
- WHEN the receiver fires and computes remaining minutes
- THEN the system clamps to 1 and displays "1 min"
- AND does NOT display a negative number or crash
---
### Requirement: L10n-Ready Notification Text
The system MUST NOT hardcode any natural-language string in the notification content.
The pre-notice message MUST be produced via a localized string resource that accepts
a `{minutes}` placeholder.
A new ARB key `preNoticeCountdown` MUST be added to all 13 locale ARB files. The
string template MUST follow the same pattern as the existing `durationMinutesOnly`
key.
#### Scenario: Device locale is Spanish
- GIVEN the device locale is `es`
- WHEN the pre-notice notification is posted
- THEN the notification body uses the Spanish translation of `preNoticeCountdown`
with the computed minutes substituted
#### Scenario: Device locale is English
- GIVEN the device locale is `en`
- WHEN the pre-notice notification is posted
- THEN the notification body uses the English translation of `preNoticeCountdown`
with the computed minutes substituted
#### Scenario: Missing locale translation (fallback)
- GIVEN the device locale has no translation for `preNoticeCountdown`
- WHEN the pre-notice notification is posted
- THEN the system falls back to the default locale translation (English)
- AND does NOT display an untranslated key name or crash
---
### Requirement: Notification ID Stability
The pre-notice notification MUST be posted with the same stable notification ID
derived from `alarmId` that the system currently uses (`notificationIdForAlarm(alarmId)`).
#### Scenario: Pre-notice posted
- GIVEN an alarm with `alarmId = X`
- WHEN the pre-notice fires
- THEN `NotificationManagerCompat.notify()` is called with ID `notificationIdForAlarm(X)`
- AND calling `notify()` again with the same ID updates the existing notification
rather than creating a duplicate
---
### Requirement: No Additional Infrastructure
The pre-notice notification MUST be a single, static notification posted once at
fire time. The system MUST NOT schedule per-minute update chains (AlarmManager
repeat), WorkManager periodic tasks, or new foreground services to update the
countdown text after posting.
#### Scenario: Notification posted
- GIVEN the pre-notice fires
- WHEN the notification is posted
- THEN exactly one `notify()` call is made
- AND no new AlarmManager intents, WorkManager jobs, or foreground services are started
@@ -0,0 +1,77 @@
# Alarm Snooze Dismiss Specification
## Purpose
Define the required behavior for dismissing the alarm screen when the user taps
Snooze. The dismissal MUST work reliably regardless of whether the app was already
running or was launched cold (dead-app state) by the full-screen intent.
## Requirements
### Requirement: Reliable Screen Dismissal on Snooze
When the user taps Snooze, the alarm screen MUST be removed from view. The system
MUST handle both a live Navigator stack (app was running) and an empty Navigator
stack (app launched from dead state via full-screen intent).
The system MUST use `Navigator.canPop()` to determine the stack state before
calling `Navigator.pop()`. If `canPop()` returns false, the system MUST call
`SystemNavigator.pop()` as a fallback to close the activity.
#### Scenario: Snooze from a running app (Navigator stack non-empty)
- GIVEN the alarm screen is displayed and the app was already running before the alarm fired
- WHEN the user taps Snooze
- THEN `Navigator.canPop()` returns true
- AND `Navigator.pop()` is called
- AND the alarm screen is dismissed
- AND the app returns to the previous screen
#### Scenario: Snooze from dead-app state (Navigator stack empty)
- GIVEN the app was not running when the alarm fired
- AND the alarm screen was launched by the full-screen intent as the root activity
- WHEN the user taps Snooze
- THEN `Navigator.canPop()` returns false
- AND `SystemNavigator.pop()` is called
- AND the alarm screen activity is closed
- AND the device returns to the home screen or the previous app
#### Scenario: Snooze side effects always complete regardless of dismiss path
- GIVEN either app state (running or dead)
- WHEN the user taps Snooze
- THEN `_liberarAudioLocal()` is called before any navigation action
- AND `radio.audio.pausar()` is called before any navigation action
- AND `alarmas.posponerAlarma()` is called before any navigation action
- AND the navigation dismissal is the LAST action in `_posponer()`
---
### Requirement: Snooze Re-Trigger Is Unaffected
The snooze re-schedule logic MUST remain unchanged. The fix MUST NOT alter when or
how `posponerAlarma()` reprograms the AlarmManager.
#### Scenario: Snooze re-trigger after dead-app dismissal
- GIVEN the alarm screen was dismissed via `SystemNavigator.pop()`
- WHEN the snoozed time (`snoozeHasta`) is reached
- THEN the AlarmManager fires the alarm again
- AND the alarm screen is shown again via full-screen intent
---
### Requirement: No Unintended App Termination
`SystemNavigator.pop()` MUST only be called when `canPop()` is false (i.e., the
screen was the root route launched from a dead-app full-screen intent). It MUST NOT
be called when a live Navigator stack exists.
#### Scenario: Guard prevents accidental SystemNavigator.pop() in running-app state
- GIVEN the app has a non-empty Navigator stack
- WHEN the user taps Snooze
- THEN `canPop()` returns true
- AND `SystemNavigator.pop()` is NOT called
- AND only `Navigator.pop()` is used for dismissal