layer-wizard/lib/confirmation_page.dart
Philip Peterson 4f1fe8f880 Keep post-reboot confirmation page in place instead of restarting the wizard
"Yes, keep this update" previously pushed a fresh WizardHomePage, which
always lands on its first step — so confirming felt like being sent
back to square one. Show a terminal "kept" message on the same page
instead, with an explicit button if the user wants to start a new
install.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-10 11:55:04 -08:00

147 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
import 'confirmation_marker.dart';
import 'history/history_service.dart';
import 'ostree_service.dart';
import 'wizard_page.dart';
class ConfirmationPage extends StatefulWidget {
final PendingConfirmation pending;
const ConfirmationPage({super.key, required this.pending});
@override
State<ConfirmationPage> createState() => _ConfirmationPageState();
}
class _ConfirmationPageState extends State<ConfirmationPage> {
bool _rollingBack = false;
bool _kept = false;
final List<String> _log = [];
final ScrollController _logScroll = ScrollController();
Future<void> _keep() async {
await HistoryService.instance.markCurrentAsSafe();
await ConfirmationMarker.clear();
await ConfirmationMarker.removeAutostart();
if (!mounted) return;
setState(() => _kept = true);
}
Future<void> _confirmRollback() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Roll back now?'),
content: const Text(
'This will immediately reboot the machine into the previous system '
'version. Save any open work first.',
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(
onPressed: () => Navigator.pop(context, true),
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error),
child: const Text('Roll back and reboot'),
),
],
),
);
if (confirmed != true) return;
setState(() => _rollingBack = true);
// The machine reboots as part of this call returning — clear bookkeeping
// first so there's nothing stale to confirm once we're back on the
// known-good deployment.
await ConfirmationMarker.clear();
await ConfirmationMarker.removeAutostart();
await OstreeService.rollback((line) {
setState(() => _log.add(line));
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_logScroll.hasClients) {
_logScroll.jumpTo(_logScroll.position.maxScrollExtent);
}
});
});
}
@override
Widget build(BuildContext context) {
final p = widget.pending;
return Scaffold(
appBar: AppBar(title: const Text('Confirm Update')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _kept
? [
Icon(Icons.check_circle, size: 48, color: Colors.green.shade600),
const SizedBox(height: 16),
Text('"${p.packageName}" kept.', style: const TextStyle(fontSize: 18)),
const SizedBox(height: 8),
const Text('Nothing else to do here.'),
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const WizardHomePage()),
),
icon: const Icon(Icons.add),
label: const Text('Install something else'),
),
]
: _rollingBack
? [
const Text('Rolling back and rebooting…', style: TextStyle(fontSize: 18)),
const SizedBox(height: 16),
Expanded(
child: Container(
color: Colors.black87,
padding: const EdgeInsets.all(8),
child: ListView.builder(
controller: _logScroll,
itemCount: _log.length,
itemBuilder: (context, i) => Text(
_log[i],
style: const TextStyle(color: Colors.white, fontFamily: 'monospace', fontSize: 12),
),
),
),
),
]
: [
Icon(Icons.help_outline, size: 48, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 16),
Text(
'You installed "${p.packageName}" on '
'${p.installedAt.toLocal().toString().split('.').first}.',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
const Text('The system rebooted into the update. Is everything working correctly?'),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text('Previous version: ${p.before.version}\n'
'Previous image: ${p.before.containerImageRef}'),
),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _keep,
icon: const Icon(Icons.check),
label: const Text('Yes, keep this update'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _confirmRollback,
icon: const Icon(Icons.undo),
label: const Text('No, something\'s wrong — roll back'),
),
],
),
),
);
}
}