mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-12 08:31:05 +01:00
The alarm was opt-in: it only armed when the user tapped "Set Alarm & Sync to PC", so it could be defeated by simply not tapping it, or by cancelling it the night before. It is now automatic and there is no lever a half-asleep user can pull that buys them sleep. - AlarmPolicy bakes the schedule in at 08:00 daily. The only edit the UI offers is moving the NEXT alarm earlier; setEarlier() refuses anything at or after the baseline, and refuses to relax an active override. Earlier-only is the whole design: every available change costs the user sleep. - The ratchet resets after each fire, so an early start is one-shot. A one-way ratchet with no reset is a trap, not a defence — the first 05:00 would otherwise be permanent with no way back short of reinstalling. - load() re-validates the stored value and ignores anything at or after the baseline. SharedPreferences is not a trust boundary. - No cancel. The Kotlin handler is deleted, not just the Dart wrapper, so the capability genuinely does not exist rather than being one invokeMethod away. - Arming happens in main()'s init seam and via AlarmWatchdog, a daily self-rescheduling check that re-arms if nothing is scheduled — closing the gap where a force-stop or a failed re-arm goes unnoticed until the morning it does not ring. BootReceiver re-establishes it, since setInexactRepeating does not survive a reboot. - Publishing to the PC is automatic too; it used to need the same tap. PC half, so the two agree with the network down: - FALLBACK_ALARM_HOUR 13 -> 8, matching the phone's baseline. At 13:00 a Firebase outage silently moved the alarm five hours LATER — the one direction the design refuses to allow. - _trigger._clamp_to_baseline() refuses any resolved alarm later than the baseline, whatever its source. Without it the PC would faithfully reproduce a stale or tampered remote value and the forbidden direction stays reachable through sync. Tests: 465 Python at 100% branch coverage, 79 Dart. Both ratchet directions are covered, per the standing rule about monotonic counters. test_sync.py and test_trigger.py were split (payload, token, CLI, clamp) to stay under the 250-line gate; the split lost indirect coverage of extract_hlc/newest_alarm, which now have direct tests instead. `python -m wake_alarm._sync --show` and `_trigger --dry-run` both re-run and still print real output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
50 lines
1.8 KiB
Dart
50 lines
1.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:wake_alarm_sync/screens/home_screen.dart';
|
|
import 'package:wake_alarm_sync/screens/ring_screen.dart';
|
|
import 'package:wake_alarm_sync/services/alarm_policy.dart';
|
|
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
|
|
import 'package:wake_alarm_sync/ui/theme.dart';
|
|
|
|
/// Route the Kotlin `AlarmActivity` hands to the engine as its initial
|
|
/// route.
|
|
const String kRingRoute = '/ring';
|
|
|
|
/// Navigator key, so the platform side can drive navigation without a context.
|
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|
|
|
/// Entry point.
|
|
///
|
|
/// `ensureInitialized` is required before any MethodChannel call: the
|
|
/// startup re-arm below runs before the first frame.
|
|
Future<void> main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
// The alarm is not opt-in: launching the app is enough to arm it. There is
|
|
// no "set alarm" button to forget, and no cancel to reach for at 02:00 —
|
|
// AlarmPolicy fixes the time at 08:00 and only ever lets it move earlier.
|
|
final policy = await AlarmPolicy.load();
|
|
await AlarmScheduler().armSchedule(policy.hour, policy.minute);
|
|
runApp(const WakeAlarmSyncApp());
|
|
}
|
|
|
|
/// Root application widget.
|
|
class WakeAlarmSyncApp extends StatelessWidget {
|
|
/// Creates a [WakeAlarmSyncApp].
|
|
const WakeAlarmSyncApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Wake Alarm Sync',
|
|
theme: buildAppTheme(),
|
|
navigatorKey: navigatorKey,
|
|
// AlarmActivity sets its initial route to kRingRoute, so the ring screen
|
|
// is what comes up over the lockscreen; the launcher still gets home.
|
|
routes: <String, WidgetBuilder>{
|
|
'/': (_) => const HomeScreen(),
|
|
kRingRoute: (_) => const RingScreen(),
|
|
},
|
|
);
|
|
}
|
|
}
|