Files
pluriwave/openspec/changes/app-quality-and-native-alarms/apply-progress.md
T
FreeTLab 202bef3539 feat(ui): design token discipline, accessibility and i18n pass
- Replace all hardcoded Color literals outside lib/tema with theme tokens (new static brand palette in PluriWaveTokens); media notification uses the brand color instead of the Material default purple
- Favorite button on station cards grows to a 48dp target and becomes an independent semantics node for screen readers (Semantics container fix)
- All flutter_animate call sites route through the PluriAnimate reduced-motion gate (zero direct .animate() left)
- Locale-aware short dates via intl DateFormat (new lib/l10n/formato_fechas.dart) replacing the hardcoded DD/MM/YYYY; proper plural messages for the favorites counter; example stream URL as a localized key - all 13 locales
- Rounded shimmer placeholders matching card radii; shimmer loading state in search instead of a bare spinner; rounded icon variants unified in settings; bottom-sheet conventions on the custom station form
- Fix latent debug crash: vacation editor read AppLocalizations in initState
- 11 new tests (121 total green), flutter analyze clean
2026-06-11 23:42:16 +02:00

70 KiB
Raw Blame History

Apply Progress: app-quality-and-native-alarms

Mode: Strict TDD (test runner: flutter test) Artifact store: openspec (Engram unavailable this session) Delivery: auto-chain, local apply — no commits, no PRs (user commits at own cadence) Last updated: 2026-06-11 (Batch 7)

Batch log

Batch Slice Status Date
1 S1 — Alarm native reliability COMPLETE (Dart verified; Kotlin/manifest on-device verification deferred to user) 2026-06-11
2 S2a + S2b — Snooze correctness end-to-end + Alarm UX parity COMPLETE (Dart verified; Kotlin on-device verification deferred to user) 2026-06-11
3 S3a + S3b — Test seams (statics/prefs/cache/mutex/dirty-guard/bounded set) + audio_session COMPLETE (Dart-only batch; call-pause on-device verification deferred to user) 2026-06-11
4 S7 — Streaming resilience (buffer config, reconnect state machine, UI wiring) COMPLETE (Dart-only batch; stream-drop on-device verification deferred to user) 2026-06-11
5 S4a — ServicioExportImport + EstadoEcualizador extraction + compat getters COMPLETE (Dart-only batch) 2026-06-11
6 S4b — EstadoGrabacion + EstadoBusqueda + scoped rebuilds + compat-getter removal COMPLETE (Dart-only batch) 2026-06-11
7 S5 — Design system, a11y, i18n, polish COMPLETE (Dart-only batch) 2026-06-11

Task status (cumulative)

Slice S1 — Alarm native reliability — 17/17 complete

Task Status Notes
T-S1-01 [x] RED test: programar() payload carries fallbackStationName/Url, fadeInSegundos, fallbackSound
T-S1-02 [x] RED test: solicitarExencionBateria() invokes requestIgnoreBatteryOptimizations
T-S1-03 [x] Manifest FGS type + permission. DEVIATION (see below)
T-S1-04 [x] API ≥ 34 3-arg startForeground with type bitmask. DEVIATION (see below)
T-S1-05 [x] Receiver showFireNotification + ensureFireChannel removed; service notification (id 92841) is sole FSI owner; fireNotificationIdForAlarm kept for cancel-migration safety
T-S1-06 [x] setFullScreenIntent was ALREADY present in service buildNotification; stopAlarm already cancels legacy fire id + stopForeground(STOP_FOREGROUND_REMOVE); FSI-before-audio ordering documented with comment
T-S1-07 [x] Channel pluriwave_alarm_fire_v2 (IMPORTANCE_HIGH, DEFAULT_ALARM_ALERT_URI + USAGE_ALARM attrs, vibration); one-time deletion of pluriwave_alarm_native + pluriwave_alarm_fire guarded by channels_migrated_v2 flag in device-protected prefs pluriwave_alarm_channels; channel count consolidated to 2 (fire_v2 + pre-notice)
T-S1-08 [x] NativeAlarmSpec + fallbackStationName/Url, fadeInSegundos; schemaVersion 2→3; fromJson backward-compatible (null/0 defaults); wired through scheduleAlarm, MainActivity handler, EXTRA_* consts, fireIntent extras
T-S1-09 [x] Three-stage chain primary(15s) → fallback station(15s) → bundled WAV via continuation lambdas (onStageFailed); scheduleStationFallback per stage with independent timeout windows
T-S1-10 [x] startFadeIn ramp: start 0.05×target, 250 ms steps over fadeInSegundos; applied to station and bundled-WAV players; cancelFadeIn() in stopAlarm (snooze path goes through stopAlarm)
T-S1-11 [x] requestIgnoreBatteryOptimizations MethodChannel handler + private method in MainActivity.kt mirroring requestExactAlarmPermission
T-S1-12 [x] GREEN: payload fields added to programar() args map
T-S1-13 [x] GREEN: solicitarExencionBateria() on PuertoAlarmasAndroid + impl
T-S1-14 [x] GREEN: asked-once guard in _solicitarPermisosNecesariosParaAlarma — calls only when !diag.ignoraOptimizacionBateria AND bateria_exencion_solicitada unset; optional SharedPreferences? prefs ctor param added to EstadoAlarmas (forward-compatible with S3 injection)
T-S1-15 [x] flutter test — full suite 54 tests, all passing (5 new)
T-S1-16 [x] flutter analyzeNo issues found! (baseline before S1 was also clean)
T-S1-17 [x] dart format applied to the 4 touched Dart files

Slice S2a — Snooze correctness — 20/20 complete

Task Status Notes
T-S2a-01 [x] RED: test/estado/estado_alarmas_snooze_test.dart — anchor, snoozed-event, recalc-preserve + extras (cold-start import, stop-cancels-snooze, finalizar clears)
T-S2a-02 [x] RED: test/servicios/servicio_alarmas_snooze_test.dart — anchor future/clamped/custom(7), payload snoozeUntilMillis+snoozeOriginMillis, getNativeSnoozeState parse/empty. Test C (finalizar) lives in the estado file
T-S2a-03 [x] RED: synchronous list update after posponerAlarma (no poll)
T-S2a-04 [x] Kotlin: ACTION_SNOOZE reports back via MainActivity.notifyAlarmEvent with alarmAction="snoozed" (PluriWaveAlarmService.kt:56-80). On-device verify
T-S2a-05 [x] Kotlin: MainActivity.notifyAlarmEvent companion (lines ~610-635), @Volatile activeInstance set in configureFlutterEngine, cleared in onDestroy; main-thread post; no-op when engine dead. On-device verify
T-S2a-06 [x] Kotlin: AlarmScheduler.snooze() (lines 266-292) unified to occurrenceAt + minutes clamped to now + minutes; persists snoozeMinutes; returns NativeSnoozeResult(until, origin, title). On-device verify
T-S2a-07 [x] Kotlin: AlarmScheduler.nativeSnoozeStates() (lines 366-385) + getNativeSnoozeState handler (MainActivity.kt:192). On-device verify
T-S2a-08 [x] GREEN: EventoAlarmaAndroid.snoozeUntilMillis + accionSnoozed; app.dart ignores snoozed events in _abrirAlarmaSonando
T-S2a-09 [x] snoozeUntilMillis was ALREADY in the programar() payload — locked by new test, no code change
T-S2a-10 [x] GREEN: _alRecibirEventoNativo (estado_alarmas.dart:266) — posponerEjecucionHasta + _aplicar + notifyListeners, NO second programar. Subscribed in the CONSTRUCTOR (deviation, see below); cancelled in dispose
T-S2a-11 [x] GREEN: _importarSnoozesNativosActivos (estado_alarmas.dart:312) called at the end of _sincronizarEjecucionesGestionadasPorAndroid; imports active future snoozes for active alarms when they differ
T-S2a-12 [x] GREEN: obtenerEstadoSnoozeNativo() on PuertoAlarmasAndroid + impl + EstadoSnoozeNativo model
T-S2a-13 [x] GREEN: _recalcular snoozeActivo now requires alarma.activa (servicio_alarmas.dart:395) — disabling clears the snooze; finalizar path already cleared + re-programs without snooze (bridge cancels natively when inactive)
T-S2a-14 [x] RED: test/pantallas/pantalla_alarma_sonando_test.dart — buttons 3/5/10(+7), no-dup, tap-5 behavior
T-S2a-15 [x] GREEN: _liberarAudioLocal() + _posponer(int) + _detener refactor (pantalla_alarma_sonando.dart:138,161). _estadoSub.cancel() is fire-and-forget (deviation, see below)
T-S2a-16 [x] GREEN: snooze button row via _opcionesSnooze() (sorted {3,5,10,custom}); NEW l10n keys in ALL 13 .arb files: alarmSnoozeOptionLabel, snoozeAction, alarmSnoozeDurationTitle, alarmFallbackStationLabel, alarmStationPickerSearchHint (+ flutter gen-l10n regenerated)
T-S2a-17 [x] Targeted snooze tests green
T-S2a-18 [x] Full suite 77/77
T-S2a-19 [x] flutter analyze — No issues found
T-S2a-20 [x] dart format on touched files

Slice S2b — Editor + visual redesign — 12/12 complete

Task Status Notes
T-S2b-01 [x] RED: scaffold test — PluriWaveScaffold present, no Color(0xFF061722) Scaffold, Animate present / absent under disableAnimations
T-S2b-02 [x] RED: 5 editor tests (preview + live update, primary picker + filtering, fallback picker, snooze duration persists, volume floor 0.0)
T-S2b-03 [x] GREEN: ringing screen on PluriWaveScaffold; 0xFFFFB86Btokens.warmCoral; blurSigma: 10 + cold-GPU comment (Design 2.4)
T-S2b-04 [x] GREEN: lib/tema/pluri_animate.dartpluriFadeIn/pluriScaleIn honoring MediaQuery.maybeDisableAnimationsOf
T-S2b-05 [x] GREEN: glass surface wrapped in .pluriFadeIn(context)
T-S2b-06 [x] GREEN: _vistaProximaEjecucion (draft → calcularProxima, respects vacations/exceptions; recomputed on every setState)
T-S2b-07 [x] GREEN: _CampoSelectorEmisora + _SelectorEmisoraSheet (SearchBar picker) for primary AND fallback station; copyWith clear-flags added (see deviations)
T-S2b-08 [x] GREEN: snooze duration SegmentedButton wired to snoozeMinutos (editor used to hardcode 5); volume slider min 0.25 → 0.0 (divisions 20)
T-S2b-09 [x] S2b targeted tests 7/7 green
T-S2b-10 [x] Full suite 77/77
T-S2b-11 [x] flutter analyze — No issues found
T-S2b-12 [x] dart format applied

Slice S3a — Test seams — 15/15 complete

Task Status Notes
T-S3a-01 [x] RED: servicio_alarmas_android_instance_test.dart — two channels, simulated alarmFired via handlePlatformMessage, isolation asserted both ways
T-S3a-02 [x] RED: servicio_alarmas_cache_test.dart_PrefsEspia implements SharedPreferences (setString/getString counters); no-write-when-clean, exactly-one-write-when-dirty, concurrent-no-lost-write (+ single cache hydration)
T-S3a-03 [x] RED: estado_alarmas_ejecuciones_test.dart — 100 stale entries pruned (1 fresh survives) + 250-entry cap test
T-S3a-04 [x] RED: mini_reproductor_configurar_test.dart — 10 rebuilds → 1 configurar; locale es→en → 2
T-S3a-05 [x] GREEN: ServicioAlarmasAndroid statics → instance fields; handler installed per instance in ctor. DEVIATION: no deprecated static shim (Dart name clash + only call site rewired in same change); configurarLocalizaciones added to PuertoAlarmasAndroid interface instead
T-S3a-06 [x] GREEN: MiniReproductor → StatefulWidget, locale-guarded didChangeDependencies; alarm-bridge l10n hoisted to app.dart _PaginaPrincipalState.didChangeDependencies (design 3.3 alternative), before the early-return
T-S3a-07 [x] GREEN: main.dart resolves prefs ONCE; PluriWaveApp(prefs:)EstadoRadio/EstadoAlarmas(→ServicioAlarmas)/EstadoIdioma
T-S3a-08 [x] GREEN: injected-with-fallback _resolverPrefs() in estado_radio (10 sites), servicio_ecualizador (6), servicio_grabacion_radio (4), servicio_contenido_app (3). rg check: only main.dart + one fallback per class remain
T-S3a-09 [x] GREEN: recalcularTodas dirty-guard — serialized comparison vs _cacheRaw, skips write when identical
T-S3a-10 [x] GREEN: _cache/_cacheRaw + _enCola writer queue; all 8 mutation methods queued over _configActual(). DEVIATION: public cargar() still re-reads prefs (queued cache reset) because EstadoRadio.importarConfig writes the raw alarms key directly — a fully cached cargar would hide imports until restart
T-S3a-11 [x] GREEN: bounded _ejecucionesEmitidas — cap 200 + 24 h retention, pruned on every add and each _vigilarAlarmasVencidas pass; @visibleForTesting length getter
T-S3a-12 [x] Targeted S3a tests green (RED first: 1 passed / 6 failed across the batch)
T-S3a-13 [x] Full suite 89/89 (77 baseline + 12 new)
T-S3a-14 [x] flutter analyze — No issues found
T-S3a-15 [x] dart format on 19 touched files

Slice S3b — audio_session + intent flag — 7/7 complete

Task Status Notes
T-S3b-01 [x] RED: servicio_audio_session_test.dart — 5 tests (pause-begin, resume-end, no-resume-without-prior-pause, duck begin/end, becoming-noisy hard pause) over fake ObjetivoAudioInterrumpible
T-S3b-02 [x] GREEN: lib/servicios/servicio_audio_session.dartmusic().copyWith(androidWillPauseWhenDucked: true); interruption + becoming-noisy subscriptions; _pausadoPorInterrupcion gate for auto-resume; defines ObjetivoAudioInterrumpible (test seam)
T-S3b-03 [x] GREEN: PluriWaveAudioHandler implements ObjetivoAudioInterrumpible; _intencionReproducir true in play()/playMediaItem(), false in pause()/stop() (S7 seam); duck via setAtenuado ×0.3 (_volumenEfectivo); configurar() wired in main.dart
T-S3b-04 [x] Targeted run 5/5 green (RED first: load failure)
T-S3b-05 [x] Full suite 89/89
T-S3b-06 [x] flutter analyze — No issues found
T-S3b-07 [x] dart format applied

Slice S7 — Streaming resilience — 13/13 complete

Task Status Notes
T-S7-01 [x] RED: servicio_audio_reconnect_test.dart — 8 tests over ControladorReconexion (backoff sequence + cap, retry scheduled with intent=true, NO retry with intent=false, exhaustion → agotado, restablecer resets counter + backoff base, cancelar kills pending timer) via injectable fake-timer factory
T-S7-02 [x] RED: buffer-config test asserts PluriWaveAudioHandler.configuracionCargaAndroid values (15s/50s/2.5s/5s, prioritizeTime=true) — construction wiring not unit-testable without platform channels (see deviations)
T-S7-03 [x] RED: reconnect_ui_test.dart — reconectando shows spinner + "Reconectando..." label, NO AlertDialog/SnackBar, NO manual-retry button; second test locks retry button to error state only
T-S7-04 [x] GREEN: _crearPlayer passes audioLoadConfiguration: configuracionCargaAndroid; named static const durations. just_audio 0.9.46 API verified in pub-cache source — all design params exist, NO deviation
T-S7-05 [x] GREEN: EstadoReproduccion.reconectando added; estadoStream maps the handler's reconectando flag (error wins, then reconectando, then cargando)
T-S7-06 [x] GREEN: NEW lib/servicios/controlador_reconexion.dart (pure logic: maxReintentos=5, base=1s, cap=30s); handler enters reconnect on network-class errors (PlayerException 2xxx OR TimeoutException) with intent=play; retry re-issues source via revision-guarded _cambiarFuente; ready+playing resets; pause/stop/playMediaItem cancel/reset; exhaustion → single terminal error; _cambiarFuente returns normally when reconnect engaged so EstadoRadio.reproducir doesn't snackbar mid-retry
T-S7-07 [x] GREEN: mini player (spinner + playbackStatusReconnecting in _labelEstado), full player (_WaveHero/_Controles: reconectando = loading, not error), visualizer stays active; l10n key in ALL 13 .arb locales + gen-l10n
T-S7-08 [x] GREEN: S7-R4 boundary comment at _estadoSub listener — only reproduciendo cancels the alarm's 12s fallback timer; reconectando never counts as playing (code already correct, now documented + locked by enum distinctness)
T-S7-09 [x] GREEN: ServicioGrabacionRadio untouched except S7-R5 invariant comment above _fallar
T-S7-10 [x] Targeted run 10/10 green (RED first: +0 -2 load failures)
T-S7-11 [x] Full suite 99/99 (89 baseline + 10 new)
T-S7-12 [x] flutter analyze — No issues found
T-S7-13 [x] dart format on 9 touched files (2 reflowed); re-ran suite + analyze after format

Slice S4a — ServicioExportImport + EstadoEcualizador — 10/10 complete

Task Status Notes
T-S4a-01 [x] RED: servicio_export_import_test.dart — v2 round-trip deep-equal (favorites/groups/EQ/alarms/vacations, alarmas raw passthrough, "sin asignar" excluded) + malformed JSON → null (invalid/empty/non-object)
T-S4a-02 [x] RED: estado_ecualizador_test.dartcambiarPreset notifies EQ listeners; EQ change does NOT notify EstadoRadio listeners (radio = 0)
T-S4a-03 [x] GREEN: lib/servicios/servicio_export_import.dartconstruirExportacion (v2 envelope, exact legacy key set), exportar (pretty JSON), importar (graceful Map?). pantalla_ajustes lost dart:convert; EstadoRadio gained exportarConfigJson/parsearConfigJson
T-S4a-04 [x] GREEN: lib/estado/estado_ecualizador.dart — full EQ state + persistence + audio application; emisoraActualUuid callback seam; ListenableProvider registration in app.dart (owned/disposed by EstadoRadio)
T-S4a-05 [x] GREEN: EstadoRadio keeps 15 delegating members, all tagged // TODO(S4b): remove getter; EQ fields and private helpers removed
T-S4a-06 [x] GREEN: _SeccionEcualizadorConsumer2<EstadoRadio, EstadoEcualizador>; ecualizador_widget.dart already presentational (no change); pantalla_reproductor EQ toggle also rewired (deviation 3)
T-S4a-07 [x] Targeted run 4/4 green (RED first: +0 -2 load failures)
T-S4a-08 [x] Full suite 103/103 (99 baseline + 4 new)
T-S4a-09 [x] flutter analyze — No issues found
T-S4a-10 [x] dart format on 8 touched files (4 reflowed); analyze + suite re-run after format

Slice S4b — EstadoGrabacion + EstadoBusqueda + scoped rebuilds — 13/13 complete

Task Status Notes
T-S4b-01 [x] RED: estado_grabacion_test.dart — 4 tests (notify on state change, iniciar delegates with current station, no-station → alError without service call, error state → alError) over a controlled ServicioGrabacionRadio fake
T-S4b-02 [x] RED: estado_busqueda_test.dart — 3 tests (notify on buscar, pagination/memory cap MOVED from estado_radio_test, identity-stable resultados getter)
T-S4b-03 [x] RED: pantalla_inicio_rebuild_test.dart — EQ preset change does NOT rebuild PantallaInicio (S4-R5-A), debugPrintRebuildDirtyWidgets probe + positive control (cargarPopulares DOES rebuild)
T-S4b-04 [x] GREEN: lib/estado/estado_grabacion.dart — owns service, subscription, dir/maxBytes/open actions, pluriwave/file_actions channel; emisoraActual+alError seams; ListenableProvider in app.dart
T-S4b-05 [x] GREEN: lib/estado/estado_busqueda.dart — search + nearby (cercanas) + min-bitrate filter; ordenListas/textos/alError seams; ListenableProvider in app.dart
T-S4b-06 [x] GREEN: pantalla_inicio — no root watch; selects over identity-memoized getters (NEW lib/estado/orden_emisoras.dart: enum + sorter + MemoLista); cercanas/genre sections on EstadoBusqueda
T-S4b-07 [x] GREEN: pantalla_ajustes 6 watch sites — Grabaciones → watch; Timer/Orden/Grupos/Preferida/Emisoras → context.select; _SeccionInfo keeps scoped Consumer
T-S4b-08 [x] GREEN: pantalla_favoritos → selects; ALSO pantalla_buscar root watch → EstadoBusqueda and pantalla_reproductor _GrabacionWidget → EstadoGrabacion (mandatory: EstadoRadio no longer notifies on recording/search)
T-S4b-09 [x] GREEN: estado_radio.dart — 15 compat members removed (zero TODO(S4b) in lib/), recording + search state extracted; 1121 (pre-split) → 753 lines
T-S4b-10 [x] Targeted run 8/8 green (RED first: +0 -3 load failures)
T-S4b-11 [x] Full suite 110/110 (103 baseline 1 moved test + 8 new)
T-S4b-12 [x] flutter analyze — No issues found
T-S4b-13 [x] dart format on 15 touched files (10 reflowed); analyze + suite re-run after

Slice S5 — Design system, a11y, i18n — 18/18 complete

Task Status Notes
T-S5-01 [x] RED: tarjeta_emisora_a11y_test.dart — semantics label/button/toggled + ≥48dp. RED exposed a REAL bug: the card InkWell merged the favorite into ONE semantics node (screen readers could not reach the action independently)
T-S5-02 [x] RED: pluri_animate_test.dart — fadeIn/scaleIn lock-in + NEW pluriFadeSlideIn (honest RED: method missing)
T-S5-03 [x] RED: pantalla_alarmas_fecha_test.dart — en-US 6/11/2026, NOT 11/06/2026; es day/month
T-S5-04 [x] RED: pantalla_favoritos_plural_test.dartstationCount singular ≠ plural (en, es)
T-S5-05 [x] RED: pantalla_buscar_shimmer_test.dart — shimmer during loading, NO spinner (cargando-true EstadoBusqueda subclass seam)
T-S5-06 [x] RED: notification_color_test.dartconfiguracionAudioService.notificationColor == PluriWaveTokens.brand
T-S5-07 [x] GREEN: zero Color(0x...) outside lib/tema (rg audit). New static tokens brand/brightCyan/auroraTeal/skyBlue; scaffold gradient/orbs, alarmas glows, visualizer stop, premium orbs, tarjeta sweep mapped; Colors.grey/green/white semantic uses → colorScheme/tokens
T-S5-08 [x] GREEN: favorite Semantics(container: true, ...) + 48×48 in both variants; _AssetIcon.semanticLabel (+excludeFromSemantics when decorative); alarm/vacation/ringing images labelled via NEW alarmIconLabel/vacationIconLabel
T-S5-09 [x] GREEN: pluriFadeSlideIn added; ALL flutter_animate call sites routed through PluriAnimate (inicio chips/grid, buscar results, reproductor ×8); zero direct .animate() in lib/
T-S5-10 [x] GREEN: NEW lib/l10n/formato_fechas.dart (fechaCortaLocalizada); _fechaCorta(l10n, fecha) delegates via l10n.localeName (6 call sites)
T-S5-11 [x] GREEN: stationCount ICU plural in ALL 13 locales (ru/ar full category sets); NEW streamUrlHint replaces hardcoded stream.ejemplo.com; gen-l10n run
T-S5-12 [x] GREEN: shimmer rounded (radiusLg/6) + NEW esCompacta row variant; PantallaBuscar loading → 4 compact shimmer rows
T-S5-13 [x] GREEN: 9 ajustes icons → _rounded; *_outlined family left (no rounded-outline variant exists); _FormularioEmisora sheet → useSafeArea + showDragHandle
T-S5-14 [x] GREEN: configuracionAudioService top-level const; notificationColor: PluriWaveTokens.brand
T-S5-15 [x] Targeted run 11/11 green (RED first: +0 -6)
T-S5-16 [x] Full suite 121/121 (110 baseline + 11 new)
T-S5-17 [x] flutter analyze — No issues found; color-literal audit ZERO
T-S5-18 [x] dart format on 20 touched Dart files (7 reflowed); analyze + suite re-run after

Remaining slices (not started)

S6, cross-cutting (T-CC-01, T-CC-02) — pending. S6 is now UNBLOCKED (depends on S4b + S5, both complete).

Snooze defect fixes (design audit D1D5 / S1S5)

Defect Fix Where
D1 (audit S1) — native snooze never notifies Flutter ACTION_SNOOZEMainActivity.notifyAlarmEvent("snoozed", origin, until); Flutter records via posponerEjecucionHasta WITHOUT re-programming; engine-dead case covered by getNativeSnoozeState cold-start import PluriWaveAlarmService.kt:56-80, MainActivity.kt:627, estado_alarmas.dart:266,312
D2 (audit S2) — two snooze anchors Unified everywhere to occurrence + minutes clamped to now + minutes: native snooze() adopts postponeNext logic; Dart posponerEjecucion re-anchored from now+min to ejecucion+min AlarmScheduler.kt:266-292, servicio_alarmas.dart:256-274
D3 (audit S3) — no snooze on ringing screen 3/5/10 + custom buttons → _posponer → shared _liberarAudioLocal teardown → canonical EstadoAlarmas.posponerAlarma (Flutter-first; hides native notification = same stop path) pantalla_alarma_sonando.dart:138-176,242-256
D4 (audit S4) — recalc erases native-only snooze Resolved transitively by D1: Flutter now RECORDS every native snooze, so _recalcular sees snoozeActivo and preserves it; regression-guard test added; additionally snooze cleared when alarm disabled (S2-R5) servicio_alarmas.dart:392-401, test estado_alarmas_snooze_test.dart
D5 (audit S5) — preserveNativeSnooze origin mismatch Resolved transitively by D1/D2: Flutter always sends snoozeUntilMillis when snoozed, so the preservation net is no longer load-bearing; kept as belt-and-suspenders per design AlarmScheduler.kt:preserveNativeSnooze (unchanged)

TDD Cycle Evidence (Strict TDD hard gate)

Task RED (test written first, failing) GREEN (implementation passes) REFACTOR
T-S1-01/T-S1-12 servicio_alarmas_android_test.dart written first; load failure + payload keys absent Payload fields added; 3 tests pass dart format
T-S1-02/T-S1-13 Same RED run: solicitarExencionBateria undefined → compile failure Interface + impl added; test passes None needed
T-S1-14 "solicita exencion una sola vez" FAILED (Expected 1, Actual 0) Asked-once guard; both tests pass Fake made configurable
T-S1-03..11 (Kotlin) N/A — on-device items N/A Surgical diffs
T-S2a-01..03 / T-S2a-08..13 All 3 new test files failed to LOAD (missing EstadoSnoozeNativo, accionSnoozed, obtenerEstadoSnoozeNativo, alarmSnoozeOptionLabel) — captured before any implementation. Anchor test would fail under old now+min semantics (verified by design: old code returned 7:05 vs expected 7:35) All bridge/state/service changes added; targeted run 23/23 green Shared FakePuertoAlarmasAndroid extracted to test/helpers/fakes_alarmas.dart; existing estado_alarmas_test.dart deduplicated
T-S2a-14..16 Widget test load failure (l10n key missing) then tap-test failure (snoozeHasta null) Buttons + _posponer implemented; 3/3 green Debug prints removed; unawaited cancel documented
T-S2b-01..02 / T-S2b-03..08 Scaffold tests failed (no PluriWaveScaffold/Animate); all 5 editor tests failed (key not found) — captured in dedicated RED run (0 passed / 7 failed) pluri_animate.dart, scaffold migration, editor preview/pickers/snooze/volume; 7/7 green Material(transparency) wrappers; initState-l10n fix; dart format

RED run evidence: first run +0 -3 (loading failures, three files); ringing tap test Expected: DateTime:<07:35> Actual: <null>; S2b run +0 -7 before implementation. GREEN: targeted 23/23 then 7/7; full suite 00:24 +77: All tests passed!.

Batch 3 TDD Cycle Evidence (S3a + S3b)

Task RED (test written first, failing) GREEN (implementation passes) REFACTOR
T-S3a-01/T-S3a-05 Static controller shared events: expect(eventosB, isEmpty) FAILED ('solo-a' leaked into B) Statics → instance fields; isolation test passes Comment documenting handler re-bind semantics
T-S3a-02-A/T-S3a-09 recalcularTodas always wrote: Expected: <1> Actual: <2> writes Dirty-guard skips clean writes _serializar extracted, shared with _guardar
T-S3a-02-B Passed pre-fix (exactly-once lock-in guard) Still passes (regression lock)
T-S3a-02-C/T-S3a-10 Lost write: final config had 1 of 2 alarms; 2 hydration reads Cache + _enCola queue: both alarms persisted, 1 hydration read Mutation bodies kept verbatim, only wrapped
T-S3a-03/T-S3a-11 Load failure: ejecucionesEmitidasLength/maxEjecucionesEmitidas undefined Bounded set: 1 survivor of 101, cap respected Prune helper shared by add-path and watch-pass
T-S3a-04/T-S3a-06 Expected: <1> Actual: <11> (configurar on every rebuild) StatefulWidget + locale guard: 1 then 2 FakeServicioAudio gained l10n override (assert fix)
T-S3b-01/02/03 Load failure: servicio_audio_session.dart missing 5/5 green against fake objetivo

RED run evidence (Batch 3): 00:06 +1 -6 before implementation (the single pass is the exactly-once write lock-in). GREEN: targeted 12/12; full suite 00:12 +89: All tests passed!.

Batch 4 TDD Cycle Evidence (S7)

Task RED (test written first, failing) GREEN (implementation passes) REFACTOR
T-S7-01/T-S7-06 Load failure: controlador_reconexion.dart missing (+0 -2 run) ControladorReconexion created; 6 decision/backoff tests pass Doc comments tying defaults to the ~60-90s design window
T-S7-02/T-S7-04 Same RED run: configuracionCargaAndroid undefined Const config + _crearPlayer wiring; values test passes Durations extracted as named static const
T-S7-03/T-S7-05/T-S7-07 Compile failure: EstadoReproduccion.reconectando missing Enum + stream mapping + UI wiring; both widget tests pass Test fixed to double-pump (stream event delivery + frame); diag run proved impl correct before the fix

RED run evidence (Batch 4): 00:00 +0 -2 (both files fail to load). GREEN: targeted 00:01 +10: All tests passed!; full suite 00:08 +99: All tests passed! (89 baseline + 10 new).

Batch 5 TDD Cycle Evidence (S4a)

Task RED (test written first, failing) GREEN (implementation passes) REFACTOR
T-S4a-01/T-S4a-03 Load failure: servicio_export_import.dart missing (+0 -2 run) Service created; round-trip + malformed tests pass Envelope comments tied to legacy format compatibility
T-S4a-02/T-S4a-04/05 Same RED run: estado_ecualizador.dart missing, estado.ecualizador undefined EQ notifier + EstadoRadio delegation; both tests pass dart format reflow; delegation kept expression-bodied

RED run evidence (Batch 5): 00:00 +0 -2 (both files fail to load — captured before any lib code). GREEN: targeted 00:00 +4: All tests passed!; full suite 00:12 +103: All tests passed! (99 baseline + 4 new); analyze + suite re-run after format.

Batch 6 TDD Cycle Evidence (S4b)

Task RED (test written first, failing) GREEN (implementation passes) REFACTOR
T-S4b-01/T-S4b-04 Load failure: estado_grabacion.dart missing (+0 -3 run) EstadoGrabacion created; 4 tests pass Comment ties callbacks to the S4a seam pattern
T-S4b-02/T-S4b-05 Same RED run: estado_busqueda.dart missing EstadoBusqueda created; 3 tests pass Pagination test deduplicated out of estado_radio_test
T-S4b-03/T-S4b-06..09 Same RED run: estado.busqueda undefined; then first GREEN attempt FAILED honestly (Expected: true Actual: <false>) because the element.dirty probe cannot observe provider's deferred dependent notification Probe rewritten over debugPrintRebuildDirtyWidgets; EQ change → screen NOT in rebuild log; cargarPopulares control → screen IS in log Memo identity test added to estado_busqueda_test locking the select-enabler invariant

RED run evidence (Batch 6): 00:00 +0 -3 (all three files fail to load — captured before any lib code). GREEN: targeted 8/8; full suite 00:11 +110: All tests passed! (103 baseline 1 moved + 8 new); analyze + suite re-run after format.

Batch 7 TDD Cycle Evidence (S5)

Task RED (test written first, failing) GREEN (implementation passes) REFACTOR
T-S5-01/T-S5-08 find.bySemanticsLabel found 0 nodes — the card InkWell MERGED the favorite into one node (real a11y defect, probed with a semantics dump) Semantics(container: true) + 48dp target; node found, button/toggled flags pass hasFlag (deprecated) → flagsCollection + Tristate; explicit semantics.dispose() (addTearDown fires too late for the handle check)
T-S5-02/T-S5-09 Compile failure: pluriFadeSlideIn undefined Helper added; 4/4 animate tests green
T-S5-03/T-S5-10 Load failure: formato_fechas.dart missing fechaCortaLocalizada + delegation; en-US/es tests pass
T-S5-04/T-S5-11 Compile failure: stationCount missing from AppLocalizations ARB plural ×13 + gen-l10n; both locale tests pass
T-S5-05/T-S5-12 TarjetaEmisoraShimmer found 0, spinner found 1 Compact shimmer variant + buscar loading swap Shimmer block builder extracted (bloque) shared by both variants
T-S5-06/T-S5-14 Compile failure: configuracionAudioService/PluriWaveTokens.brand undefined Const extraction + brand token; test passes electricMagenta re-pointed at brand (no duplicate literal)

RED run evidence (Batch 7): 00:02 +0 -6 (4 compile/load failures + 2 honest assertion failures) captured before any lib code. GREEN: targeted 11/11; full suite 00:14 +121: All tests passed!; analyze + suite re-run after dart format.

Files changed (Batch 7)

File Action ~Lines
lib/tema/pluriwave_tokens.dart Modified +12 (static brand/brightCyan/auroraTeal/skyBlue token definitions; format reflow)
lib/tema/pluri_animate.dart Modified +14 (pluriFadeSlideIn)
lib/l10n/formato_fechas.dart Created +14 (locale-aware short date)
lib/main.dart Modified +13/-8 (configuracionAudioService const, brand notification color)
lib/widgets/pluri_wave_scaffold.dart Modified +8/-8 (gradient + orbs → tokens/colorScheme)
lib/widgets/tarjeta_emisora.dart Modified +60/-30 (brightCyan, favorite container semantics + 48dp, rounded shimmer + compact variant)
lib/widgets/visualizador_audio.dart Modified +2/-1 (warmCoral token)
lib/widgets/pluri_premium_widgets.dart Modified +3/-2 (brightCyan)
lib/pantallas/pantalla_alarmas.dart Modified +55/-30 (glow tokens, _AssetIcon semanticLabel, locale-aware _fechaCorta, vacaciones initState l10n fix)
lib/pantallas/pantalla_favoritos.dart Modified +2/-1 (plural counter)
lib/pantallas/pantalla_buscar.dart Modified +18/-7 (shimmer loading, pluriFadeSlideIn)
lib/pantallas/pantalla_ajustes.dart Modified +20/-12 (rounded icons, semantic colors, l10n hint, sheet conventions)
lib/pantallas/pantalla_reproductor.dart Modified +25/-20 (PluriAnimate routing ×8, white overlays → colorScheme/tokens)
lib/pantallas/pantalla_inicio.dart Modified +6/-3 (PluriAnimate routing)
lib/l10n/app_*.arb (13 files) Modified +4-5 each (stationCount plural, alarmIconLabel, vacationIconLabel, streamUrlHint)
lib/l10n/gen/* (14 files) Regenerated by flutter gen-l10n
test/widgets/tarjeta_emisora_a11y_test.dart Created +72 (1 test)
test/tema/pluri_animate_test.dart Created +80 (4 tests)
test/tema/notification_color_test.dart Created +17 (1 test)
test/pantallas/pantalla_alarmas_fecha_test.dart Created +29 (2 tests)
test/pantallas/pantalla_favoritos_plural_test.dart Created +21 (2 tests)
test/pantallas/pantalla_buscar_shimmer_test.dart Created +42 (1 test)

Total Batch 7 lib diff: ~240 insertions / ~120 deletions (incl. ARB, excl. gen/), plus ~260 lines of new tests. Within the ~210-line slice estimate for hand-written lib changes. No Kotlin/native files touched.

Deviations from design (Batch 7)

  1. Semantics(container: true) on the favorite button (not in task text). The task only asked for Semantics(button, label) — which ALREADY existed. The RED test proved the real defect: the card-level InkWell merged the favorite into a single semantics node, so screen readers could not reach the action. container: true forces an own node; this is the actual S5-R2 fix.
  2. fechaCortaLocalizada lives in NEW lib/l10n/formato_fechas.dart (task said edit _fechaCorta in place). The private top-level function is untestable from flutter test; the public helper takes a locale tag (testable without widgets) and _fechaCorta(l10n, fecha) delegates via l10n.localeName. Spec scenario S5-R4-A is met verbatim.
  3. Brand token is PluriWaveTokens.brand, not brandColor (task text guessed the name). Defined as a static const so it works in the const AudioServiceConfig context; electricMagenta now references it (single source for 0xFF21D4D9). brightCyan/auroraTeal/skyBlue added the same way for palette colors that had no token.
  4. Colors.white overlays mapped to colorScheme.onSurface/onPrimary and tokens.glassBorder — the scheme's onSurface is 0xFFF2F7FA (near-white), so visuals are intentionally near-identical while becoming theme-driven.
  5. pluriScaleIn on the hero adds a fade the old bare .scale() did not have (the shared helper pairs fade+scale). Visually negligible at 420 ms; keeping one canonical scale-entry beats a third helper.
  6. Icon variants: only base-name icons got _rounded (9 sites). The *_outlined Material family (folder_outlined, backup_outlined, upload/download_outlined, verified_outlined, music_note_outlined) has NO rounded-outline variant; switching them to filled _rounded would change their visual weight, so they stay outlined.
  7. streamUrlHint added as an l10n key (the prompt's "handled per design"): the hardcoded hint leaked a Spanish-looking host (stream.ejemplo.com) into all 13 locales; the key ships the same neutral stream.example.com URL everywhere but is now localizable.
  8. _EditorVacacionesSheet initState l10n crash fixed (flagged in Batch 2 as a latent debug-mode crash, explicitly brought into this batch's scope): controller now created lazily in didChangeDependencies, mirroring the alarm-editor fix.
  9. Pagination spinner in PantallaBuscar kept — S5-R6 covers the initial loading state (explore C11, lines 241-245); the small inline load-more spinner is a different affordance and was left untouched.
  10. flagsCollection/Tristate instead of SemanticsFlag.hasFlag in the a11y test — hasFlag is deprecated in the current SDK and flutter analyze flags it; also tester.ensureSemantics() must be disposed in the test body (an addTearDown runs after the framework's handle-leak check).

Files changed (Batch 2)

File Action ~Lines
android/.../AlarmScheduler.kt Modified +57/-12 (unified snooze + NativeSnoozeResult + nativeSnoozeStates)
android/.../MainActivity.kt Modified +36/-1 (notifyAlarmEvent companion, activeInstance, getNativeSnoozeState)
android/.../PluriWaveAlarmService.kt Modified +19/-1 (snooze report-back)
lib/servicios/servicio_alarmas_android.dart Modified +60 (snoozed event, EstadoSnoozeNativo, bridge method)
lib/servicios/servicio_alarmas.dart Modified +20/-3 (anchor, activa-aware snooze clearing)
lib/estado/estado_alarmas.dart Modified +95/-15 (event subscription, snooze recording, cold-start import)
lib/pantallas/pantalla_alarma_sonando.dart Modified +70/-15 (snooze buttons, teardown, PluriWaveScaffold, tokens, fade-in)
lib/pantallas/pantalla_alarmas.dart Modified ~+330/-60 net (preview, pickers, snooze field, volume floor, initState fix, Material wrappers; large diff partly dart-format reflow)
lib/modelos/alarma_musical.dart Modified +10/-2 (limpiarEmisora/limpiarEmisoraFallback)
lib/app.dart Modified +7 (ignore snoozed events)
lib/tema/pluri_animate.dart Created +39
lib/l10n/app_*.arb (13 files) Modified +12 each (5 keys + metadata)
lib/l10n/gen/* (15 files) Regenerated by flutter gen-l10n
test/helpers/fakes_alarmas.dart Created +120
test/helpers/fakes.dart Modified +13 (pausar/setVolumen on FakeServicioAudio)
test/estado/estado_alarmas_snooze_test.dart Created +250
test/servicios/servicio_alarmas_snooze_test.dart Created +155
test/pantallas/pantalla_alarma_sonando_test.dart Created +165
test/pantallas/pantalla_alarma_sonando_scaffold_test.dart Created +150
test/pantallas/pantalla_alarmas_editor_test.dart Created +210
test/estado/estado_alarmas_test.dart Modified -78/+8 (fake deduplicated to helper; anchor expectations 7:36:00 → 7:36:02)

Files changed (Batch 3)

File Action ~Lines
lib/servicios/servicio_audio_session.dart Created +115 (session config, interruption/noisy handling, ObjetivoAudioInterrumpible)
lib/servicios/servicio_alarmas.dart Modified +106/-64 (cache, _enCola queue, dirty-guard, _parsear/_serializar)
lib/estado/estado_alarmas.dart Modified +55/-2 (bounded set, configurarLocalizaciones, prefs→servicio default)
lib/servicios/servicio_alarmas_android.dart Modified +18/-11 (statics → instance, interface method)
lib/servicios/servicio_audio.dart Modified +52/-3 (intent flag, ObjetivoAudioInterrumpible impl, duck volume)
lib/estado/estado_radio.dart Modified +16/-15 (prefs param + _resolverPrefs, static alarm-l10n call removed)
lib/widgets/mini_reproductor.dart Modified +22/-2 (StatefulWidget, locale-guarded didChangeDependencies)
lib/main.dart Modified +14/-2 (prefs once, ServicioAudioSession wiring)
lib/app.dart Modified +22/-3 (PluriWaveApp.prefs, alarm l10n locale guard)
lib/servicios/servicio_ecualizador.dart Modified +14/-6 (prefs injection)
lib/servicios/servicio_grabacion_radio.dart Modified +13/-4 (prefs injection)
lib/servicios/servicio_contenido_app.dart Modified +11/-3 (prefs injection)
test/helpers/fakes.dart Modified +8 (configurarLocalizaciones override on FakeServicioAudio)
test/helpers/fakes_alarmas.dart Modified +4 (interface no-op)
test/servicios/servicio_alarmas_android_instance_test.dart Created +53
test/servicios/servicio_alarmas_cache_test.dart Created +105
test/estado/estado_alarmas_ejecuciones_test.dart Created +85
test/widgets/mini_reproductor_configurar_test.dart Created +85
test/servicios/servicio_audio_session_test.dart Created +130

Total Batch 3 diff: ~362 insertions / ~122 deletions in lib + helpers, plus ~458 lines of new tests.

Files changed (Batch 4)

File Action ~Lines
lib/servicios/controlador_reconexion.dart Created +100 (decision enum + backoff controller, injectable timer factory)
lib/servicios/servicio_audio.dart Modified +148/-7 (enum reconectando, buffer config consts, reconnect integration, TimeoutException routing, pause/stop/play resets)
lib/widgets/mini_reproductor.dart Modified +6/-2 (spinner for reconectando, label case)
lib/pantallas/pantalla_reproductor.dart Modified +8/-2 (reconectando = loading in _WaveHero + _Controles)
lib/widgets/visualizador_audio.dart Modified +2/-1 (reconectando keeps visualizer active)
lib/pantallas/pantalla_alarma_sonando.dart Modified +5 (S7-R4 boundary comment)
lib/servicios/servicio_grabacion_radio.dart Modified +4 (S7-R5 invariant comment)
lib/l10n/app_*.arb (13 files) Modified +1 each (playbackStatusReconnecting)
lib/l10n/gen/* (14 files) Regenerated by flutter gen-l10n
test/servicios/servicio_audio_reconnect_test.dart Created +210 (8 tests)
test/widgets/reconnect_ui_test.dart Created +100 (2 tests)

Total Batch 4 diff: ~235 insertions / ~13 deletions in lib (incl. ARB/gen), plus ~310 lines of new tests. Within the ~285-line slice estimate. No Kotlin/native files touched.

Files changed (Batch 5)

File Action ~Lines
lib/servicios/servicio_export_import.dart Created +80 (v2 envelope builder, pretty export, graceful parse)
lib/estado/estado_ecualizador.dart Created +205 (EQ ChangeNotifier: presets, per-station map, activo, persistence, audio application, import path)
lib/estado/estado_radio.dart Modified +122/-208 (EQ state/methods extracted; 15 // TODO(S4b) delegating members; exportarConfig delegates envelope; importarConfig delegates EQ; exportarConfigJson/parsearConfigJson; ecualizador owned + disposed)
lib/pantallas/pantalla_ajustes.dart Modified +33/-23 (backup section delegates JSON to service, dart:convert removed; _SeccionEcualizador → Consumer2 with EstadoEcualizador)
lib/pantallas/pantalla_reproductor.dart Modified +8/-11 (EQ toggle watches EstadoEcualizador)
lib/app.dart Modified +7 (ListenableProvider exposing EstadoRadio's instance)
test/servicios/servicio_export_import_test.dart Created +85 (2 tests)
test/estado/estado_ecualizador_test.dart Created +52 (2 tests)

Total Batch 5 diff: ~455 insertions / ~242 deletions in lib, plus ~137 lines of new tests. Slightly over the ~350-line slice estimate because the EQ method bodies moved (not duplicated) into the new notifier — net lib growth is ~+213. No Kotlin/native files touched.

Files changed (Batch 6)

File Action ~Lines
lib/estado/orden_emisoras.dart Created +55 (OrdenEmisoras enum moved here + ordenarEmisoras + MemoLista identity memo; estado_radio re-exports the enum so existing imports keep compiling)
lib/estado/estado_grabacion.dart Created +144 (recording notifier: service ownership, subscription, dir/maxBytes, open-file/dir actions, file_actions channel)
lib/estado/estado_busqueda.dart Created +222 (search notifier: query/filters/pagination, cercanas + geolocation, min-bitrate filter, memoized sorted views)
lib/estado/estado_radio.dart Modified +154/-375 net (recording/search/EQ-compat removed; memoized list getters; creates+disposes the 3 notifiers; custom-station mutations now reassign for memo identity) — 753 lines final (was ~1121 pre-split)
lib/app.dart Modified +10/-4 (ListenableProviders for EstadoGrabacion + EstadoBusqueda)
lib/pantallas/pantalla_inicio.dart Modified ~+55/-41 (root watch removed; selects + EstadoBusqueda sections)
lib/pantallas/pantalla_buscar.dart Modified ~+30/-27 (root watch → watch; renamed members)
lib/pantallas/pantalla_favoritos.dart Modified ~+12/-3 (root watch → selects)
lib/pantallas/pantalla_ajustes.dart Modified ~+50/-32 (Grabaciones → EstadoGrabacion; 5 sections → selects)
lib/pantallas/pantalla_reproductor.dart Modified ~+18/-10 (_GrabacionWidget → watch)
test/estado/estado_grabacion_test.dart Created +122 (4 tests)
test/estado/estado_busqueda_test.dart Created +67 (3 tests)
test/pantallas/pantalla_inicio_rebuild_test.dart Created +97 (1 test, S4-R5-A)
test/estado/estado_radio_test.dart Modified EQ call sites → estado.ecualizador.*; pagination test moved out
test/pantallas/pantalla_inicio_test.dart Modified _conProviders helper mirrors app.dart wiring (3 pump sites)

Total Batch 6 lib diff: ~386 insertions / ~625 deletions across 9 pre-existing files plus 3 new lib files (+421) and 3 new test files (+286). Net lib growth ≈ +180; EstadoRadio shrank by ~260 lines this batch. No Kotlin/native, .arb or gen/ files touched.

Deviations from design (Batch 6)

  1. Provider ownership NOT inverted — documented as accepted. Design 217 allows "pass the shared service instances at construction"; EstadoEcualizador/EstadoGrabacion/EstadoBusqueda need EstadoRadio's services AND callbacks (emisoraActual, alError, ordenListas, textos) at construction, so EstadoRadio creates and disposes all three and the ListenableProviders only expose the instances (S4a deviation 2 pattern, now final). Inverting would require lifting ServicioAudio/ServicioRadio creation into app.dart — out of slice budget and blast radius.
  2. NEW lib/estado/orden_emisoras.dart (not in task text). Two reasons: (a) the OrdenEmisoras enum is needed by both EstadoRadio and EstadoBusqueda without a circular import (estado_radio re-exports it, so consumers compile unchanged); (b) MemoLista — derived-list getters used to return a fresh copy per read, which would make every context.select degrade to watch behavior (lists compare by identity). Identity-memoized getters are the enabler that makes S4-R5's "stop rebuilding on buffer events" REAL, not just formal.
  3. EstadoBusqueda also owns the nearby-stations (cercanas) flow (task text only said query/results/loading). cercanas shares the min-bitrate filter and radio.buscar plumbing with search; leaving it in EstadoRadio would have kept a search-state remnant there against S4-R3's intent.
  4. pantalla_buscar and pantalla_reproductor rewired beyond the task list (tasks named inicio/ajustes/favoritos). Mandatory, not optional: EstadoRadio no longer notifies on search or recording changes, so any screen still reading them through EstadoRadio would go permanently stale. Buscar now watches EstadoBusqueda; the player's _GrabacionWidget watches EstadoGrabacion.
  5. Custom-station mutations reassign the backing list instead of mutating in place — required so the identity memo (and therefore select) sees the change. Behavior identical.
  6. element.dirty is NOT a valid rebuild probe with provider — provider defers dependent notification to the next build phase (markNeedsNotifyDependents → inherited element rebuild → dependents marked during build). The widget test uses debugPrintRebuildDirtyWidgets log capture with a positive control instead. Worth remembering for future rebuild-scope tests.
  7. emisorasDisponiblesPreferencia staleness window (minor, accepted): the preferred-station dropdown's option list now refreshes when favoritos/custom/populares/tendencias change identity, but a pure search/cercanas update no longer rebuilds the section (EstadoRadio does not notify on those anymore). The options re-derive on the section's next rebuild; preferred-station resolution itself prefers favorites, so impact is cosmetic.

Deviations from design (Batch 5)

  1. importar() returns Map<String, dynamic>?, not a ConfiguracionCompleta model (task text suggested one). EstadoRadio's importarConfig(Map) is the existing application API with v1/v2 branching and a localized version-guard error; introducing a typed model would force re-validating/re-mapping every section twice in a slice that must stay under budget. The service's contract (graceful null on malformed, version inside the map) covers S4-R4; a typed model can land with S4b/S6 if wanted.
  2. ListenableProvider instead of ProxyProvider for EstadoEcualizador registration. The notifier needs ServicioAudio from EstadoRadio at CONSTRUCTION; EstadoRadio therefore constructs and disposes it (transition ownership), and the provider only exposes the instance (create: ctx.read<EstadoRadio>().ecualizador, no dispose callback — avoids double-dispose). In S4b, when EstadoRadio sheds the remaining EQ surface, ownership can be inverted if desired.
  3. pantalla_reproductor.dart EQ toggle rewired in S4a (task listed only ecualizador_widget + pantalla_ajustes). EstadoRadio no longer notifies on EQ changes (required by S4-R1-A test B), so any screen still reading EQ through EstadoRadio's compat getters under watch<EstadoRadio> would go STALE on toggle. The reproductor button was the only such site; 8-line fix beats shipping a known visual bug until S4b.
  4. ecualizador_widget.dart needed NO change: both widgets in it are presentational (preset/onCambio props, no provider reads), so T-S4a-06's intent (scoped consumption) is satisfied at the call sites in pantalla_ajustes.
  5. Compat getters do NOT relay EQ notifications to EstadoRadio listeners — intentional and spec-mandated (S4-R1-A scenario). EQ-displaying UI was rewired in this same slice precisely because of this; S4b removes the getters entirely.
  6. emisoraActualUuid callback seam on EstadoEcualizador (not in task text): per-station preset decisions need the currently playing station; a String? Function() injected by EstadoRadio keeps the notifier free of station-list coupling and trivially testable.

Deviations from design (Batch 3)

  1. No deprecated static shim for ServicioAlarmasAndroid.configurarLocalizaciones (task text asked for one). Dart forbids a static and an instance member with the same name in one class, and the ONLY external call site (estado_radio.dart:74) was rewired in this same slice — a renamed shim would be unreachable dead code. Instead configurarLocalizaciones joined the PuertoAlarmasAndroid interface (fakes no-op it).
  2. Alarm-bridge l10n configured from app.dart, not from MiniReproductor — design 3.3 offered both options; the mini player now only configures its real dependency (EstadoRadio), and _PaginaPrincipalState.didChangeDependencies (locale-guarded, placed BEFORE the existing early-return) forwards l10n to EstadoAlarmas.
  3. Public ServicioAlarmas.cargar() re-reads from prefs instead of serving the cache. EstadoRadio.importarConfig writes the raw alarmas_musicales_v1 key directly to SharedPreferences; a fully cached cargar() would make a settings import invisible until app restart. Mutations DO use the cache (_configActual), which is what S3-R7's race fix and "one cargar per mutation burst" require. The re-read is queued, so it cannot interleave with a mutation.
  4. Duck handling added beyond the task text: setAtenuado on the handler scales effective volume ×0.3 (restored on interruption end). With androidWillPauseWhenDucked: true Android delivers duck as pause, so this is mostly the iOS/edge path; kept minimal.
  5. _PrefsEspia implements SharedPreferences via noSuchMethod rather than pulling shared_preferences_platform_interface into the tests — avoids a depend_on_referenced_packages lint on a transitive dep.
  6. servicio_contenido_app.dart also migrated (3 getInstance sites; not named in the task). Its only construction site is static final in pluri_onboarding_dialog.dart, which keeps the fallback path at runtime — acceptable under the injected-with-fallback compat net; full injection there would require a dialog refactor out of S3 scope.
  7. Two-instances-same-channel semantics documented, not prevented: with instance handlers, constructing a second ServicioAlarmasAndroid over the SAME MethodChannel re-binds the platform handler to the newest instance. Production creates exactly one per channel (provider singleton); tests use distinct channels.

Deviations from design (Batch 4)

  1. Reconnect logic lives in a NEW file lib/servicios/controlador_reconexion.dart, not inline in servicio_audio.dart (task text said "edit servicio_audio.dart"). Pure decision/backoff logic must be testable without platform channels (S7-R7); the handler cannot be constructed in tests (AudioPlayer hits MethodChannels at construction). The handler keeps only the integration glue.
  2. Buffer-config test asserts the config VALUES, not the construction call. Asserting that AudioPlayer(...) received the config would require constructing the real player (platform channels). The config is a static const on the handler; _crearPlayer passes it (one-line wiring, verified by review + on-device item 9). Honest scope of S7-R1's [flutter test] portion.
  3. TimeoutException treated as network-class in addition to the spec'd PlayerException 2xxx range. A real network drop usually surfaces as the existing 12s source-change timeout, NOT as a 2xxx PlayerException — without this, the most common stall would bypass reconnect entirely. The generic-Exception terminal path is otherwise unchanged.
  4. Stall detection is error-driven only (per task T-S7-06 text); the design's optional "buffering > 8-10s watchdog" was NOT implemented. ExoPlayer/just_audio surfaces dead live streams as errors or our timeout; a buffering watchdog would add a timer racing the buffer config for marginal gain. Flagged for on-device validation (item 9): if a silent endless-buffering hang is observed, add the watchdog in a follow-up.
  5. _cambiarFuente completes normally when reconnect is engaged (returns instead of rethrowing). Previously every failure rejected the playMediaItem future and EstadoRadio.reproducir's catch pushed an error snackbar — that would show an error on the FIRST failure even while reconnecting, violating S7-R3. User-facing rejection still happens when reconnect does NOT engage (non-network error, no intent, exhausted).
  6. restablecer() on playMediaItem (fresh user play/source switch restarts the backoff budget). Not explicit in the task text but required so the retry path (which goes through _cambiarFuente internally, not playMediaItem) can exhaust while user-initiated switches always get a full budget.
  7. Widget tests need a double tester.pump() after a broadcast-stream emission (one pump delivers the event, the second rebuilds). Verified with a diagnostic harness that the implementation was correct and only the test needed the fix.

Deviations from design (Batch 2)

  1. Event subscription lives in the EstadoAlarmas CONSTRUCTOR, not inicializar (task text said inicializar). Rationale: snoozed events must be recorded even before/without full init, and tests with iniciarAutomaticamente: false stay light. Cancelled in dispose.
  2. posponerEjecucion clamps minutes to 1..120 instead of coercing to 3/5/10. The old calcularSnooze coercion would have made the custom snooze button (e.g. "7 min", S2-R1-C) silently snooze 5. The native notification path still sanitizes to 3/5/10 (sanitizeSnoozeMinutes), unchanged. calcularSnooze kept (unused by this path) for API compatibility.
  3. Existing test expectation updated (estado_alarmas_test.dart): unified anchor makes snooze land at proximaEjecucion(+inminencia normalization) + 5min = 7:36:02, not now+5 = 7:36:00. This is the spec'd behavior change (S2-R6), documented inline.
  4. _recalcular now clears snooze for INACTIVE alarms — required by S2-R5-A ("snoozeHasta is null in persistent storage" after disable); previously a disabled alarm kept a stale snoozeHasta forever.
  5. AlarmaMusical.copyWith gained limpiarEmisora/limpiarEmisoraFallback (not in task text). Without them the picker's "no station" could never clear an existing station (latent pre-existing bug: copyWith null-coalesced).
  6. Pre-existing debug crash fixed: _EditorAlarmaSheetState.initState called AppLocalizations.of(context)dependOnInheritedWidgetOfExactType assert in debug builds. Name controller now created lazily in didChangeDependencies. (The sibling _EditorVacacionesSheet has the same latent issue — NOT fixed here, out of S2 scope; flag for S5/S6.)
  7. Material(type: transparency) wrappers added inside PluriGlassSurface for the editor sheet and the station picker — ListTiles inside a DecoratedBox trigger a debug assert and invisible ink splashes otherwise.
  8. _liberarAudioLocal does NOT await _estadoSub.cancel() — a broadcast-subscription cancel future may not resolve until the stream closes (observed in tests); cancellation of delivery is synchronous, so fire-and-forget (unawaited) is correct and prevents the snooze tap from stalling.
  9. AlarmScheduler.snooze returns NativeSnoozeResult (until/origin/title) so the service can build the report-back payload; postponeNext untouched (already had the unified anchor).
  10. Ringing screen blurSigma capped to 10 (PluriGlassSurface default 18) as the Design 2.4 cold-GPU mitigation, plus reduced-motion users skip the entry animation entirely.

Issues found

  • flutter test does NOT auto-run gen-l10n in this setup despite generate: true; flutter gen-l10n must be run manually after editing .arb files (gen files are committed).
  • tester.tap + an awaited broadcast-subscription cancel() deadlocks the gesture handler chain in widget tests (see deviation 8) — worth remembering for S3/S7 work.

On-device verification checklist for the user

From tasks.md Section 11 — S1 items still pending from Batch 1, plus new S2 items:

  1. Kotlin compiles — native layer edited again without compilation (HIGH risk): build FIRST.
  2. Alarm fires app-killed on Android 14+ (S1-R1/R2); channel v2 on alarm stream (S1-R3); 3-stage fallback (S1-R4); native fade (S1-R6); battery dialog once (S1-R5); reboot persistence.
  3. Snooze from ringing screen (S2-R1, S2-R4): ring → tap "5 min" → notification dismissed, list shows snoozeHasta immediately, re-fires at that time.
  4. Snooze from notification while app killed (S2-R3): tap "Posponer" on the fire notification with the app killed → system alarm icon persists → reopen the app → list shows the snooze WITHOUT waiting for the 60 s poll (cold-start getNativeSnoozeState import).
  5. Snooze from notification while app foregrounded (S2-R3): same, but the list updates within the same frame via the snoozed MethodChannel event.
  6. Stop cancels pending snooze (S2-R5): snooze → disable the alarm from the list → does NOT re-fire.
  7. Ringing screen visuals (S2-R7): PluriWaveScaffold gradient + entry fade; verify no first-frame stutter on screen-off FSI wake (blur capped); with "remove animations" accessibility setting the entry is instant.
  8. Editor (S2-R8..R11): next-trigger preview updates live; searchable pickers for primary AND backup station; snooze duration control; volume slider reaches 0%.

Verification summary (Batch 2)

  • flutter test: 77/77 passing (54 pre-batch + 23 new)
  • flutter analyze: No issues found (identical to baseline)
  • dart format: applied to all touched Dart files only (gen/ untouched by hand)
  • flutter gen-l10n: run once after .arb edits
  • flutter build: NOT run (forbidden)

Verification summary (Batch 3)

  • flutter test: 89/89 passing (77 baseline + 12 new across 5 files)
  • flutter analyze: No issues found (identical to baseline)
  • dart format: applied to all 19 touched Dart files (5 reflowed)
  • rg 'SharedPreferences.getInstance()' lib/: only main.dart startup resolution + one injected-with-fallback expression per class (estado_alarmas, estado_idioma, estado_radio, servicio_alarmas, servicio_ecualizador, servicio_grabacion_radio, servicio_contenido_app)
  • flutter build: NOT run (forbidden)
  • No Kotlin/native files touched in this batch

On-device verification items added by Batch 3 (user — Android device)

  1. Phone call pauses radio (S3-R1, checklist item 10): while the radio plays, receive a call → radio pauses (or ducks); after the call ends it resumes automatically (transient loss).
  2. Headphones unplugged pauses radio (S3-R1): unplug wired headphones / disconnect BT while playing → radio pauses and does NOT auto-resume.
  3. Another media app takes focus: start playback in another app → PluriWave pauses; it must not resume on its own when focus is permanent loss.
  4. Locale switch sanity: change app language in Ajustes → alarm titles/station names sent to new native schedules use the new language (l10n now configured per locale change, not per rebuild).
  5. Settings import still reflects alarms immediately (cache bypass in cargar()): import a backup with alarms → the alarms list shows them without restarting the app.

Verification summary (Batch 4)

  • flutter test: 99/99 passing (89 baseline + 10 new across 2 files); re-run after dart format
  • flutter analyze: No issues found (identical to baseline); re-run after format
  • dart format: applied to all 9 touched Dart files (2 reflowed); gen/ untouched by hand
  • flutter gen-l10n: run once after the 13 .arb edits
  • flutter build: NOT run (forbidden)
  • No Kotlin/native files touched in this batch (S7-R4: native alarm audio path untouched by construction)

What the buffer actually buys (honest expectations, Design 7.1)

  • Configured: ExoPlayer keeps a 15-50s forward buffer; playback (re)starts after 2.5s buffered (5s after a rebuffer); time prioritized over byte thresholds.
  • Real drop ≲ buffered cushion (typically a few seconds up to ~15-30s depending on bitrate and how full the buffer was): audio keeps playing through the cushion, no UI change.
  • Drop longer than the cushion: playback stalls → "Reconectando..." spinner state (no error dialog/snackbar) → up to 5 backoff retries (1/2/4/8/16s delays + 12s attempt timeout each, total window ≈ up to ~90s) → on recovery the player rejoins the LIVE edge (live radio has no rewind — the missed audio is gone, not replayed) → on exhaustion, the single existing friendly error with manual retry.

On-device verification items added by Batch 4 (user — Android device)

  1. Short drop plays through (S7-R1, checklist item 9): while the radio plays (let it run ~1 min so the buffer fills), disable WiFi/LTE for ~10s → audio continues without interruption and no UI state change.
  2. Long drop reconnects (S7-R2/R3, checklist item 9): disable connectivity ~45s → mini player and full player show "Reconectando..." with spinner (NO error dialog/snackbar); re-enable within ~90s → playback resumes at the live edge automatically.
  3. Exhaustion surfaces one error (S7-R2-C): leave connectivity off >2 min → exactly ONE error state with the manual retry button appears after retries exhaust; no error spam during the retry window.
  4. User pause/stop during reconnect (S7-R6): trigger a drop, then tap pause/stop while "Reconectando..." → playback stays stopped; it must NOT restart on its own when connectivity returns.
  5. Alarm fallback not delayed (S7-R4, checklist item 11): alarm with a non-responding station URL → bundled WAV fires within the existing ~12-15s window, NOT extended by reconnect attempts.
  6. Recording during a drop (S7-R5): record while forcing a drop → recording fails/stops cleanly per existing behavior; a subsequent recording start works.
  7. Sleep timer during a drop (S7-R6): sleep timer expiring during "Reconectando..." stops audio for good.

Verification summary (Batch 5)

  • flutter test: 103/103 passing (99 baseline + 4 new across 2 files); re-run after dart format
  • flutter analyze: No issues found (identical to baseline); re-run after format
  • dart format: applied to all 8 touched Dart files (4 reflowed)
  • flutter build: NOT run (forbidden)
  • No Kotlin/native, .arb or gen/ files touched in this batch

Manual verification items added by Batch 5 (user)

  1. Backup round-trip on device (S4-R4): export a backup from Ajustes, wipe/reinstall (or import on a second device), import the file → favorites, groups, custom stations, EQ presets, alarms (incl. vacations) and preferences all restored. Old (pre-S4a) backup files must import identically — the v2 envelope is byte-compatible.
  2. EQ controls still live-update (S4-R1): toggle EQ from the player screen and from Ajustes; chip/switch/preset selector reflect changes immediately (these now rebuild from EstadoEcualizador, not EstadoRadio).
  3. Per-station preset on playback switch: play a station with its own preset, switch to one without → main preset re-applies (path now goes through EstadoEcualizador).

Verification summary (Batch 6)

  • flutter test: 110/110 passing (103 baseline 1 pagination test moved to estado_busqueda_test + 8 new across 3 files); re-run after dart format
  • flutter analyze: No issues found (identical to baseline) — used as the safety net for missed call sites after removing the 15 compat members; re-run after format
  • dart format: applied to all 15 touched Dart files (10 reflowed)
  • rg 'TODO\(S4b\)' lib/: ZERO occurrences (only historical mentions in tasks.md/apply-progress.md remain)
  • EstadoRadio final size: 753 lines (was ~1121 pre-split, ~1010 after S4a)
  • flutter build: NOT run (forbidden)
  • No Kotlin/native, .arb or gen/ files touched in this batch

Manual verification items added by Batch 6 (user)

  1. Search screen (S4-R3): search by name/country/language/quality, infinite scroll, genre chips on home — results and spinners behave as before (now driven by EstadoBusqueda).
  2. Nearby stations (S4-R3): "Detect" on home requests location and fills the nearby strip; error text when undetectable.
  3. Recording (S4-R2): start/stop from the player (indefinite, fixed and custom durations), live duration/bytes counter updates, open-folder/open-last-file buttons, recordings settings section (change/restore dir, max size) — all now via EstadoGrabacion.
  4. Scoped rebuilds (S4-R5): while audio plays/buffers, home/favorites/settings should feel identical (no visual change expected — the win is fewer rebuilds); list reordering in Ajustes still re-sorts home, search results and favorites.
  5. Stop recording on pause/stop/station switch: unchanged orchestration in EstadoRadio — verify recording stops when playback pauses/stops or station changes.

Verification summary (Batch 7)

  • flutter test: 121/121 passing (110 baseline + 11 new across 6 files); re-run after dart format
  • flutter analyze: No issues found (identical to baseline); re-run after format
  • dart format: applied to all 20 touched Dart files (7 reflowed); gen/ untouched by hand
  • flutter gen-l10n: run once after the 13 .arb edits
  • Color-literal audit: rg 'Color(0x' lib excluding lib/tema/ and lib/l10n/gen/0 matches (was 14)
  • rg '.animate()' lib → 0 direct flutter_animate call sites (all via PluriAnimate)
  • flutter build: NOT run (forbidden)
  • No Kotlin/native files touched in this batch

Manual verification items added by Batch 7 (user)

  1. Visual parity sweep (S5-R1): home/buscar/favoritos/ajustes/alarmas/reproductor look unchanged — token mapping was value-preserving (onSurface ≈ white; electricMagenta = brand). The settings "check" icons are now mint (colorScheme.secondary) instead of Material green — intentional.
  2. TalkBack (S5-R2): on a station card, the favorite toggle is now reachable as its OWN button ("Añadir a favoritos / Quitar de favoritos") separate from the card; alarm/vacation images announce their labels.
  3. Reduced motion (S5-R3): with "remove animations" enabled, home grid/search results/player screens render instantly with no entry animations.
  4. Locale dates (S5-R4): switch app language to English → alarm editor and vacation ranges show M/D/Y order; Japanese shows Y/M/D.
  5. Media notification (S5-R8): the playback notification accent is teal (brand), not purple, on devices that honor notificationColor.

Workload / boundary

  • Mode: auto-chain local slices (no PRs)
  • Current work units: S1, S2a, S2b, S3a, S3b, S7, S4a, S4b (committed, latest 52855e7), S5 (complete, in working tree)
  • Boundary (Batch 7): starts from the clean post-52855e7 tree; ends with S5 fully checked off, suite green (121/121). Rollback = revert the 27 modified lib files (incl. 13 ARB + gen/) + delete the 7 new files (Dart-only; no native edits).
  • Next batch: S6 (quality gates — lint hardening + remaining top-5 tests) then cross-cutting T-CC-01/T-CC-02. S6 is the LAST slice.