wake-alarm/phone_app/lib/services/enforcement_state.dart
Krzysztof kuhy Rudnicki 34037e55c1 phone: sick days — picker, silent ring, stick stand-down, pre-alarm sync
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later
  day to mark it sick, tap again to clear; past days inert. Writes the
  sickdays wire format (state sick/cleared + actor, one HLC per write) to
  sickdays-sync/devices via crdt_sync syncLog.
- Mutual exclusion in both pickers: each syncs the other pool on open and
  refuses a date it holds, naming the pool; an unreadable pool refuses all.
- Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the
  ring with reason "sick_day" (no markFired, no ring_started); StickCheck
  verdict sick_day, streak skips sick days; stakes card says so.
- Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request
  code 1006) runs a headless engine (poolSyncMain) that pulls both pools
  from RTDB and rewrites both native caches, so a PC mark arrives with the
  app closed. The ring decision stays a cache read.
- JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision,
  PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check.

pylint hook skipped: verify.py imports sickdays, not in the hook env until
the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
2026-10-16 13:22:40 +02:00

114 lines
3.5 KiB
Dart

import 'package:wake_alarm_sync/services/iso_time.dart';
/// The native stick, as the stakes card and the ring line read it.
///
/// Parsed from `EnforcementChannel.kt`'s `enforcementState` map. Every value
/// is decided natively (`StickCheck.kt`); Dart only displays it, so the date
/// on screen is the date a miss would actually lock through.
class EnforcementState {
/// Creates a state; every field mirrors one key of the native map.
const EnforcementState({
required this.today,
required this.dismissedToday,
required this.escapedToday,
required this.freeToday,
required this.freeTomorrow,
required this.streak,
this.sickToday = false,
this.sickTomorrow = false,
this.alarmHour,
this.alarmMinute,
this.lockedThrough,
this.lastMiss,
this.updatedAt,
this.lockIfMissToday,
this.lockIfMissTomorrow,
});
/// Parses the native map; null when it is absent or malformed.
static EnforcementState? fromMap(Map<Object?, Object?>? map) {
if (map == null) return null;
final today = _date(map['today']);
if (today == null) return null;
return EnforcementState(
today: today,
alarmHour: map['alarmHour'] as int?,
alarmMinute: map['alarmMinute'] as int?,
dismissedToday: map['dismissedToday'] == true,
escapedToday: map['escapedToday'] == true,
freeToday: map['freeToday'] == true,
freeTomorrow: map['freeTomorrow'] == true,
sickToday: map['sickToday'] == true,
sickTomorrow: map['sickTomorrow'] == true,
streak: (map['streak'] as int?) ?? 0,
lockedThrough: _date(map['lockedThrough']),
lastMiss: _date(map['lastMiss']),
updatedAt: map['updatedAt'] as String?,
lockIfMissToday: _date(map['lockIfMissToday']),
lockIfMissTomorrow: _date(map['lockIfMissTomorrow']),
);
}
/// The phone's local date when this was read.
final DateTime today;
/// The stored daily alarm `A`, or null when none is armed.
final int? alarmHour;
/// See [alarmHour].
final int? alarmMinute;
/// Whether today's ring was dismissed.
final bool dismissedToday;
/// Whether today was excused through the escape hatch.
final bool escapedToday;
/// Whether today is a free day in the native cache.
final bool freeToday;
/// Whether tomorrow is a free day in the native cache.
final bool freeTomorrow;
/// Whether today is a sick day in the native cache.
final bool sickToday;
/// Whether tomorrow is a sick day in the native cache.
final bool sickTomorrow;
/// Consecutive stick-day misses.
final int streak;
/// The current lockdown's last day, or null when none was sent.
final DateTime? lockedThrough;
/// The last stick day that was missed.
final DateTime? lastMiss;
/// ISO instant of the last change to the stick, or null before any.
final String? updatedAt;
/// What a miss today would lock through.
final DateTime? lockIfMissToday;
/// What a miss tomorrow would lock through, given the state as it is.
final DateTime? lockIfMissTomorrow;
/// The `stick_state` field's value (`DOCS-wake-enforcement.md`).
Map<String, Object?> toStickStateJson() => <String, Object?>{
'streak': streak,
'locked_through': _iso(lockedThrough),
'last_miss': _iso(lastMiss),
};
static DateTime? _date(Object? value) {
if (value is! String) return null;
final parsed = DateTime.tryParse(value);
return parsed == null
? null
: DateTime(parsed.year, parsed.month, parsed.day);
}
static String? _iso(DateTime? day) => day == null ? null : isoDate(day);
}