fix(alarm): stop corrupt entries and unreadable payloads from wiping saved alarms
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m38s

A single malformed alarm entry (bad/missing id, wrong type) used to
discard the ENTIRE persisted list on next load, and a fully unparseable
payload let the periodic recalculation silently overwrite it with an
empty one -- both destroyed valid alarms with no user action.

Adds a shared per-entry tolerant-parse helper (persistencia_tolerante.dart)
that skips and logs only the bad entry; ServicioAlarmas now normalizes its
cached raw after a partial load (no dirty-guard thrash) and sets a
degraded-read flag after a total decode failure that suppresses automatic
writes until a good read or an explicit user mutation restores authority.
This commit is contained in:
2026-07-11 12:27:39 +02:00
parent 23ab3494a7
commit 65c1ac2085
5 changed files with 514 additions and 24 deletions
+73 -24
View File
@@ -5,6 +5,7 @@ import 'package:uuid/uuid.dart';
import '../modelos/alarma_musical.dart';
import '../modelos/emisora.dart';
import 'persistencia_tolerante.dart';
import 'servicio_programacion_alarmas.dart';
class ConfiguracionAlarmas {
@@ -41,6 +42,14 @@ class ServicioAlarmas {
String? _cacheRaw;
Future<void> _cola = Future<void>.value();
// persistence-resilience (D4): set when the top-level payload could not
// be decoded at all (vs. an individual entry inside it). While true, the
// AUTOMATIC writer (recalcularTodas) must never reach disk -- only an
// explicit user mutation (which always funnels through _guardar) may
// overwrite a payload we could not read. Cleared by _guardar and by the
// next successful (even if partial) read.
bool _lecturaAlarmasDegradada = false;
Future<T> _enCola<T>(Future<T> Function() accion) {
final resultado = _cola.then((_) => accion());
_cola = resultado.then((_) {}, onError: (_) {});
@@ -63,12 +72,29 @@ class ServicioAlarmas {
final raw = prefs.getString(_keyConfig);
final config = _parsear(raw);
_cache = config;
_cacheRaw = raw;
return config;
}
/// Parses the persisted [raw] payload with per-entry tolerance
/// (persistence-resilience D1/D2) and updates [_cacheRaw] /
/// [_lecturaAlarmasDegradada] as a side effect, since each outcome needs
/// a DIFFERENT cached-raw value:
/// - top-level decode failure (bad JSON, or any container-level field of
/// the wrong shape) -> DEGRADED: empty config, [_cacheRaw] keeps the
/// corrupt [raw] untouched, [_lecturaAlarmasDegradada] set so
/// `recalcularTodas` never overwrites it (D4).
/// - decodes, but one or more entries are malformed -> PARTIAL: only the
/// survivors are kept, [_cacheRaw] is normalized to their own
/// serialization -- NOT the corrupt raw (D3) -- so the automatic
/// writer's dirty-guard compares against a coherent baseline instead of
/// re-firing on every 60s tick.
/// - fully healthy payload -> unchanged behavior; clears the flag if a
/// previous read had set it (suppression lifts on next successful
/// read).
ConfiguracionAlarmas _parsear(String? raw) {
if (raw == null || raw.trim().isEmpty) {
_lecturaAlarmasDegradada = false;
_cacheRaw = raw;
return const ConfiguracionAlarmas(
alarmas: [],
vacaciones: [],
@@ -77,30 +103,44 @@ class ServicioAlarmas {
}
try {
final data = jsonDecode(raw) as Map<String, dynamic>;
return ConfiguracionAlarmas(
alarmas:
(data['alarmas'] as List? ?? const [])
.whereType<Map>()
.map(
(e) => AlarmaMusical.fromJson(Map<String, dynamic>.from(e)),
)
.toList(),
vacaciones:
(data['vacaciones'] as List? ?? const [])
.whereType<Map>()
.map(
(e) => RangoVacaciones.fromJson(Map<String, dynamic>.from(e)),
)
.toList(),
excepciones:
(data['excepciones'] as List? ?? const [])
.whereType<Map>()
.map(
(e) => ExcepcionAlarma.fromJson(Map<String, dynamic>.from(e)),
)
.toList(),
final alarmas = parseListaTolerante<AlarmaMusical>(
data['alarmas'] as List?,
AlarmaMusical.fromJson,
subsistema: 'alarmas',
coleccion: 'alarmas',
);
final vacaciones = parseListaTolerante<RangoVacaciones>(
data['vacaciones'] as List?,
RangoVacaciones.fromJson,
subsistema: 'alarmas',
coleccion: 'vacaciones',
);
final excepciones = parseListaTolerante<ExcepcionAlarma>(
data['excepciones'] as List?,
ExcepcionAlarma.fromJson,
subsistema: 'alarmas',
coleccion: 'excepciones',
);
final config = ConfiguracionAlarmas(
alarmas: alarmas.validas,
vacaciones: vacaciones.validas,
excepciones: excepciones.validas,
);
_lecturaAlarmasDegradada = false;
final huboSaltos =
alarmas.saltadas > 0 ||
vacaciones.saltadas > 0 ||
excepciones.saltadas > 0;
_cacheRaw = huboSaltos ? _serializar(config) : raw;
return config;
} catch (e) {
_lecturaAlarmasDegradada = true;
_cacheRaw = raw;
registrarSaltoPersistencia(
subsistema: 'alarmas',
detalle: _keyConfig,
razon: e.toString(),
);
} catch (_) {
return const ConfiguracionAlarmas(
alarmas: [],
vacaciones: [],
@@ -200,6 +240,11 @@ class ServicioAlarmas {
Future<ConfiguracionAlarmas> recalcularTodas() => _enCola(() async {
final config = await _configActual();
// persistence-resilience (D4): a degraded top-level read must never
// let this AUTOMATIC writer reach disk -- only an explicit user
// mutation (guardarAlarma/eliminarAlarma/etc., via _guardar) may
// persist over a payload we could not read.
if (_lecturaAlarmasDegradada) return config;
final alarmas = _recalcularLista(
config.alarmas,
config.vacaciones,
@@ -427,6 +472,10 @@ class ServicioAlarmas {
await prefs.setString(_keyConfig, serializado);
_cache = config;
_cacheRaw = serializado;
// persistence-resilience (D4): every explicit mutation funnels through
// here, so this is where write authority is restored after a degraded
// read -- user intent wins over a suppressed automatic writer.
_lecturaAlarmasDegradada = false;
}
String _serializar(ConfiguracionAlarmas config) => jsonEncode({