wake-alarm/phone_app/lib/services/free_day_sync.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

50 lines
2.0 KiB
Dart

import 'package:crdt_sync/crdt_sync.dart';
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
import 'package:wake_alarm_sync/services/day_pool_sync.dart';
import 'package:wake_alarm_sync/services/firebase_backend.dart';
import 'package:wake_alarm_sync/services/free_day_pool.dart';
/// Moves the free-day pool between this phone and every other device.
///
/// Firebase only. The alarm record keeps its GitHub mirror because that
/// fallback is load-bearing for un-migrated installs; this namespace is new
/// and has no such history, so it gets no second transport.
///
/// Every sync ends by handing the merged pool to the native ring path
/// ([AlarmScheduler.setFreeDays]) — that cache, not the Dart store, is what
/// decides whether the phone rings, so it must never lag a sync.
class FreeDaySync {
/// Creates a syncer; both seams are injectable for tests.
FreeDaySync({
Future<RemoteStore?> Function()? openClient,
AlarmScheduler? scheduler,
}) : _openClient = openClient ?? openFirebase,
_scheduler = scheduler ?? AlarmScheduler();
final Future<RemoteStore?> Function() _openClient;
final AlarmScheduler _scheduler;
/// Pulls, merges and pushes [pool], then refreshes the native cache.
///
/// Never throws; see [syncDayPoolLog]. The native cache is refreshed even
/// when the sync could not run, so a day marked offline still stands the
/// phone down.
Future<String> sync(FreeDayPool pool) => syncDayPoolLog(
openClient: _openClient,
deviceId: pool.deviceId,
pathPrefix: FreeDayPool.pathPrefix,
filename: FreeDayPool.filename,
commitMessage: 'freedays: update pool',
snapshot: pool.snapshot,
replaceAll: pool.replaceAll,
publish: () => publishToNative(pool),
logName: 'FreeDaySync',
);
/// Writes the pool's free dates where `AlarmReceiver` can read them.
Future<void> publishToNative(FreeDayPool pool) => publishQuietly(
() => _scheduler.setFreeDays(pool.freeIsoDates()),
logName: 'FreeDaySync',
);
}