wake-alarm/phone_app/lib/screens/home_history_card.dart
Krzysztof kuhy Rudnicki c3c129b068 Make the morning session the carrot: earn 11:00 by finishing it in time
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
2026-09-19 16:40:51 +02:00

96 lines
3.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
}