mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-12 07:50:50 +01:00
Some checks failed
Meals logged on the phone stopped reaching the PC, so the gate kept locking for slots that were already logged and the only way through was to spam "nothing". The PC's fetch was never broken: it was faithfully pulling a Firebase that did not contain the phone's meals. The phone had no working publish backstop on either transport: - `checkAndNotify` gated its sync on `due.isNotEmpty`. Logging every meal promptly means nothing is ever due, so the ~15-min periodic task never published. It was a notification job that happened to sync, not a sync timer. It now syncs on every tick it is asked to, then computes due slots from the merged result, so a meal pulled from another device suppresses a nag instead of triggering one. - `backgroundSyncPush` never called `initSyncDeviceId()`. A fresh isolate has its own static state, so `currentSyncDeviceId` fell back to the compile-time 'phone' constant and published to `devices/phone/` rather than the persisted uuid -- which then makes the next foreground push see `unchanged` and skip writing the uuid directory entirely. Separately, on the "Fetch from sync" path itself: `FirebaseSyncError` and `FirebaseAuthError` are siblings of `GitHubSyncError` under `RemoteSyncError`, not subclasses, so every per-request failure of the *primary* backend escaped `pull_shared_log`'s catch tuple and raised a traceback out of a helper documented to fail closed with a reason string. `ConfigError` needed the same treatment by a different route: it subclasses `Exception` directly, so it is outside `RemoteSyncError` too, and is now translated at `_client_for_run`. The PAT is no longer mandatory either. Firebase is primary, but `_read_token` ran before the client was ever built, so a Firebase-only machine could not sync at all -- the commits that fixed this touched only the Dart side. A stalled sync was invisible: the config gates returned silently and `openFirebase` swallowed every error. `dart:developer`'s `log()` does not reach logcat on a release build (verified on device), so logging alone would not have surfaced it. `SyncHealth` records each tick's outcome and the log screen shows a banner when publishing has actually stopped. Also make the real-Tk probes bypass the window manager. A tiling WM retiles a managed toplevel instead of honouring the requested geometry, so `winfo_height` reported 0/1px and the layout and focus-ring assertions failed on noise rather than on any regression -- 8 failures on the dev machine that only passed under Xvfb. `overrideredirect` makes the measurement deterministic with or without a WM; the probes still detect a genuine overflow at 1366x300. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RT8X32S9yDLW1Xf1PDq8z7
82 lines
3.7 KiB
Dart
82 lines
3.7 KiB
Dart
/// The periodic due/missing-slot check, shared by every platform's scheduler.
|
|
///
|
|
/// Re-runs the same due/missing-slot logic diet_guard's `_gate.py` uses to
|
|
/// decide whether to lock the PC, and syncs notifications to match. Kept in
|
|
/// its own library, free of any scheduler plugin, because the two platforms
|
|
/// drive it very differently: Android hands it to WorkManager as a real
|
|
/// background isolate, while the browser-hosted desktop app can only run it
|
|
/// from an in-page timer while its window is open (see
|
|
/// `background_tasks_web.dart`).
|
|
library;
|
|
|
|
import 'package:diet_guard_app/models/slot.dart';
|
|
import 'package:diet_guard_app/services/background_sync_service.dart';
|
|
import 'package:diet_guard_app/services/log_storage_service.dart';
|
|
import 'package:diet_guard_app/services/notification_service.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
/// Reads the local log, computes today's due-but-unlogged slots as of
|
|
/// [now] (defaults to the real clock), and syncs notifications to match.
|
|
///
|
|
/// Syncs on every tick it is asked to ([pullWhenDue], the WorkManager
|
|
/// caller's default), then computes due slots from the merged result.
|
|
///
|
|
/// Deliberately NOT gated on "a slot looks due": for the periodic caller this
|
|
/// is the phone's only publish path, mirroring the PC's
|
|
/// `diet-guard-sync.timer`.
|
|
/// Gating the sync on `due.isNotEmpty` meant that promptly logging every meal
|
|
/// on the phone left nothing ever due, so the phone never pushed and the PC
|
|
/// kept locking for slots that were already logged -- the exact failure this
|
|
/// check exists to prevent. A notification job that happens to sync is not a
|
|
/// sync timer.
|
|
///
|
|
/// Order matters: the sync runs *before* [missingSlots], so a meal pulled from
|
|
/// another device suppresses a nag rather than triggering one.
|
|
///
|
|
/// Set [pullWhenDue] false when the caller has just written to the local log
|
|
/// itself (the log screen). There the local copy is by definition the freshest
|
|
/// thing in existence, so a pull would only duplicate the screen's own
|
|
/// auto-sync and delay dismissing the notification.
|
|
///
|
|
/// [now] is injectable so a test does not depend on the wall-clock hour it
|
|
/// happens to run at, the same reason `slot.dart`'s functions are clock-free.
|
|
/// [httpClient] is injectable for the same reason [backgroundSyncPush] takes
|
|
/// one: the real plugin path only exists in a background isolate on-device.
|
|
Future<void> checkAndNotify({
|
|
DateTime? now,
|
|
http.Client? httpClient,
|
|
bool pullWhenDue = true,
|
|
}) async {
|
|
await LogStorageService.init();
|
|
final at = now ?? DateTime.now();
|
|
if (pullWhenDue) {
|
|
// Reuses backgroundSyncPush's settings/isConfigured/try-catch guard rather
|
|
// than reimplementing it: that path is already proven to work from a
|
|
// WorkManager isolate, including the token read and singleton init.
|
|
await backgroundSyncPush(httpClient: httpClient);
|
|
}
|
|
final due = missingSlots(
|
|
at,
|
|
await LogStorageService.instance.loggedSlotsToday(),
|
|
);
|
|
await _reconcileNotifications(due);
|
|
}
|
|
|
|
/// Posts/cancels notifications for [due], swallowing platform failures.
|
|
///
|
|
/// Best-effort by design, and for both callers: from the log screen the meal
|
|
/// is already written by this point and must not be lost to a notification
|
|
/// hiccup, and from the WorkManager tick a throw would put the task into a
|
|
/// retry-with-backoff loop over something purely cosmetic. `on Object`
|
|
/// rather than `on Exception` because `flutter_local_notifications` reports
|
|
/// an absent platform channel as a `LateInitializationError`, which is an
|
|
/// `Error`.
|
|
Future<void> _reconcileNotifications(List<int> due) async {
|
|
try {
|
|
await NotificationService.init();
|
|
await NotificationService.instance.syncToSlots(due);
|
|
} on Object {
|
|
// Notifications are a nicety; nothing above depends on them.
|
|
}
|
|
}
|