fix(alarmas): heal alarms already poisoned by the old Detener anchor

The anchor fix stops NEW damage, but devices that ran the buggy build
still carry a future occurrence in ultimaEjecucionGestionada in
SharedPreferences. _esValida rejects any candidate matching it, so the
affected alarm would keep skipping that day with nothing in the UI to
explain it -- which reads as "still broken" rather than "fixed".

_recalcular now drops an ultimaEjecucionGestionada that is meaningfully
in the future. An occurrence cannot have been handled before it happens,
so such a value is corrupt by definition, and dropping it can only ever
restore a real future ring: the double-fire guard it also feeds needs a
PAST occurrence to do its job, and those are untouched.

Placed in the recalculation that every load and every mutation already
funnels through, so an affected alarm heals on the next app open with no
user action -- no delete-and-recreate.

Tests: 1122 -> 1124, including one proving a genuine past occurrence is
still preserved.
This commit is contained in:
2026-08-03 22:04:37 +02:00
parent df9252a621
commit 04300592e0
2 changed files with 70 additions and 2 deletions
+23 -2
View File
@@ -568,13 +568,34 @@ class ServicioAlarmas {
alarma.activa &&
alarma.snoozeHasta != null &&
alarma.snoozeHasta!.isAfter(ahora);
// Self-heal for state poisoned before the Detener anchor fix: a stop
// that closed a FUTURE occurrence wrote it into
// ultimaEjecucionGestionada, and _esValida rejects any candidate
// matching it -- so the alarm silently skips that day forever after,
// with nothing in the UI to explain it. An occurrence cannot have been
// handled before it happens, so a value meaningfully in the future is
// corrupt by definition and safe to drop: it can only ever suppress a
// real future ring, never prevent a double-fire (which needs a PAST
// occurrence to guard). Placed here, in the recalculation every load and
// every mutation already funnels through, so an affected alarm heals on
// the next app open with no user action.
final gestionada = alarma.ultimaEjecucionGestionada;
final gestionadaCorrupta =
gestionada != null &&
gestionada.isAfter(
ahora.add(ServicioProgramacionAlarmas.toleranciaDisparoInminente),
);
final saneada =
gestionadaCorrupta
? alarma.copyWith(limpiarUltimaEjecucionGestionada: true)
: alarma;
final proxima = _programacion.calcularProxima(
alarma: alarma,
alarma: saneada,
desde: ahora,
vacaciones: vacaciones,
excepciones: excepciones,
);
return alarma.copyWith(
return saneada.copyWith(
proximaEjecucion: proxima,
limpiarProximaEjecucion: true,
limpiarSnooze: !snoozeActivo,