mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
On 2026-09-19 the phone was powered off after the bathroom tag and nothing on the PC noticed: every finished session was published and nothing read it. Now wake-alarm distils the phone's morning into an HMAC-signed ~/.local/state/wake_alarm/morning_session.json (wake-alarm-session.timer, 07:00-11:00 every 2 min) that screen-locker and leetcode-guard read to defer their locks -- provisionally until min(start + 90 min, 09:30) while the session is live, until 11:00 once it completed inside that cutoff, and not at all for a session finished later (the phone switched on at noon completes the legs then; that must un-lock nothing). The phone publishes the session at start as well as at finish (current_session, cleared with the finish stamp), so the PC can tell "started, not finished" from "never started". A required 1-5 energy tap joins the weigh-in and ships as latest_energy with a PC reader mirroring _weight (the shared half is _field_reader). Also fixes the vmbox harness, red since the 2026-09-11 home-tidy: the guest tree lives at ~/wake-alarm, and the INSTALL-OK grep matched vm's echo of the command line. Takes Flutter 3.47.5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
96 lines
3.1 KiB
Dart
96 lines
3.1 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:wake_alarm_sync/services/morning_session.dart';
|
||
import 'package:wake_alarm_sync/services/session_store.dart';
|
||
import 'package:wake_alarm_sync/services/weight_entry.dart';
|
||
|
||
/// How many mornings the home screen shows.
|
||
const int kHistoryShown = 7;
|
||
|
||
/// The last week of mornings: outcome, re-rings and weight, one line each.
|
||
///
|
||
/// Self-loading, keyed by the home screen's sync epoch so a history a pull
|
||
/// just restored appears without a cold start. Absent entirely until the
|
||
/// first session has finished — an empty card would only say "not yet".
|
||
class HomeHistoryCard extends StatefulWidget {
|
||
/// Creates the card; [store] is injectable for tests.
|
||
const HomeHistoryCard({super.key, this.store});
|
||
|
||
/// Session persistence; defaults to SharedPreferences.
|
||
final SessionStore? store;
|
||
|
||
@override
|
||
State<HomeHistoryCard> createState() => _HomeHistoryCardState();
|
||
}
|
||
|
||
class _HomeHistoryCardState extends State<HomeHistoryCard> {
|
||
List<MorningSession> _sessions = const <MorningSession>[];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
unawaited(_load());
|
||
}
|
||
|
||
Future<void> _load() async {
|
||
final history = await (widget.store ?? SessionStore()).history();
|
||
if (!mounted) return;
|
||
setState(() => _sessions = history.take(kHistoryShown).toList());
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (_sessions.isEmpty) return const SizedBox.shrink();
|
||
final theme = Theme.of(context);
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: <Widget>[
|
||
Text('Last mornings', style: theme.textTheme.titleMedium),
|
||
const SizedBox(height: 8),
|
||
for (var i = 0; i < _sessions.length; i++)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||
child: Text(
|
||
describeMorning(_sessions[i], previousWeight: _previousWeight(i)),
|
||
style: theme.textTheme.bodyMedium,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// The nearest older weigh-in, for the delta.
|
||
double? _previousWeight(int index) {
|
||
for (var i = index + 1; i < _sessions.length; i++) {
|
||
final kg = _sessions[i].weightKg;
|
||
if (kg != null) return kg;
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// One line for [session]: `09-16 ✓ 0 re-rings · 72.4 kg (−0.3) · energy 3/5`.
|
||
String describeMorning(MorningSession session, {double? previousWeight}) {
|
||
final mark = switch (session.outcome) {
|
||
SessionOutcome.completed => '✓',
|
||
SessionOutcome.failed => '✗',
|
||
SessionOutcome.active => '…',
|
||
};
|
||
final rings = '${session.reRings} re-ring${session.reRings == 1 ? '' : 's'}';
|
||
final kg = session.weightKg;
|
||
var weight = '';
|
||
if (kg != null) {
|
||
weight = ' · $kg kg';
|
||
if (previousWeight != null) {
|
||
final delta = kg - previousWeight;
|
||
final sign = delta > 0 ? '+' : (delta < 0 ? '−' : '±');
|
||
weight += ' ($sign${delta.abs().toStringAsFixed(1)})';
|
||
}
|
||
}
|
||
final energy = session.energy == null
|
||
? ''
|
||
: ' · energy ${session.energy}/$maxEnergy';
|
||
return '${session.date.substring(5)} $mark $rings$weight$energy';
|
||
}
|