feat(doctor): update banner via HubAdmin.CheckUpdate (v0.9.0)
Doctor page now opens with a slim banner above the summary strip
in two cases: an actually newer version on the channel ("Update
available — v0.x.y", success-tone "new" pill, release-notes URL
when present) or the release host being unreachable
("Release host unreachable", neutral "offline" pill, with the
network reason). When local matches latest the banner stays
hidden — quiet UI is the right default.
Hooks into the existing parallel-fetch in `HubService.doctor` so
the new RPC adds zero latency to the page load.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
753db4fc60
commit
18db6ff164
5 changed files with 113 additions and 3 deletions
|
|
@ -210,12 +210,16 @@ class HubService {
|
|||
(_) => VerifyEventChainResponse(),
|
||||
),
|
||||
_client.listServices().catchError((_) => <DeclaredService>[]),
|
||||
_client.checkUpdate().catchError(
|
||||
(_) => CheckUpdateResponse(),
|
||||
),
|
||||
]);
|
||||
|
||||
final caps = results[0] as List<CapabilityEntry>;
|
||||
final approvals = results[1] as List<PendingApprovalEntry>;
|
||||
final chain = results[2] as VerifyEventChainResponse;
|
||||
final services = results[3] as List<DeclaredService>;
|
||||
final update = results[4] as CheckUpdateResponse;
|
||||
|
||||
// Module count = distinct module_name across capabilities.
|
||||
final moduleNames = <String>{for (final c in caps) c.moduleName};
|
||||
|
|
@ -237,10 +241,40 @@ class HubService {
|
|||
),
|
||||
)
|
||||
.toList(),
|
||||
update: UpdateStatus(
|
||||
channel: update.channel,
|
||||
localVersion: update.localVersion,
|
||||
latestVersion: update.latestVersion,
|
||||
updateAvailable: update.updateAvailable,
|
||||
manifestReachable: update.manifestReachable,
|
||||
releaseNotesUrl:
|
||||
update.releaseNotesUrl.isEmpty ? null : update.releaseNotesUrl,
|
||||
reason: update.reason.isEmpty ? null : update.reason,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateStatus {
|
||||
final String channel;
|
||||
final String localVersion;
|
||||
final String latestVersion;
|
||||
final bool updateAvailable;
|
||||
final bool manifestReachable;
|
||||
final String? releaseNotesUrl;
|
||||
final String? reason;
|
||||
|
||||
const UpdateStatus({
|
||||
required this.channel,
|
||||
required this.localVersion,
|
||||
required this.latestVersion,
|
||||
required this.updateAvailable,
|
||||
required this.manifestReachable,
|
||||
this.releaseNotesUrl,
|
||||
this.reason,
|
||||
});
|
||||
}
|
||||
|
||||
class DoctorSnapshot {
|
||||
final int moduleCount;
|
||||
final int capabilityCount;
|
||||
|
|
@ -249,6 +283,7 @@ class DoctorSnapshot {
|
|||
final int eventChainVerified;
|
||||
final String? eventChainTamperedAt;
|
||||
final List<ServiceEntry> services;
|
||||
final UpdateStatus update;
|
||||
|
||||
const DoctorSnapshot({
|
||||
required this.moduleCount,
|
||||
|
|
@ -258,6 +293,7 @@ class DoctorSnapshot {
|
|||
required this.eventChainVerified,
|
||||
required this.eventChainTamperedAt,
|
||||
required this.services,
|
||||
required this.update,
|
||||
});
|
||||
|
||||
bool get chainHealthy => eventChainTamperedAt == null;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import 'widgets/widgets.dart';
|
|||
/// Studio's own build version. Bump on every UI commit so the
|
||||
/// running app self-identifies — visible in the sidebar header
|
||||
/// and quick-glance proof that you're seeing the current build.
|
||||
const String kStudioVersion = '0.8.0';
|
||||
const String kStudioVersion = '0.9.0';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
|
|||
|
|
@ -59,9 +59,14 @@ class _DoctorPageState extends State<DoctorPage> {
|
|||
);
|
||||
}
|
||||
final s = snapshot.data!;
|
||||
final showUpdate = s.update.updateAvailable ||
|
||||
(!s.update.manifestReachable &&
|
||||
s.update.localVersion.isNotEmpty);
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||
children: [
|
||||
if (showUpdate) _UpdateBanner(status: s.update),
|
||||
if (showUpdate) const SizedBox(height: FaiSpace.lg),
|
||||
_SummaryStrip(snapshot: s),
|
||||
const SizedBox(height: FaiSpace.xl),
|
||||
_Section(
|
||||
|
|
@ -431,3 +436,72 @@ class _ServicesPanel extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateBanner extends StatelessWidget {
|
||||
final UpdateStatus status;
|
||||
|
||||
const _UpdateBanner({required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final available = status.updateAvailable;
|
||||
final iconData = available
|
||||
? Icons.system_update_alt_outlined
|
||||
: Icons.cloud_off_outlined;
|
||||
final iconColor = available
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant;
|
||||
final title = available
|
||||
? 'Update available — ${status.latestVersion}'
|
||||
: 'Release host unreachable';
|
||||
final body = available
|
||||
? 'Channel ${status.channel} now offers ${status.latestVersion}. '
|
||||
'Apply with: fai update apply --channel ${status.channel}'
|
||||
: (status.reason ?? 'Could not contact the release feed for ${status.channel}.');
|
||||
return FaiCard(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(iconData, size: 20, color: iconColor),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
body,
|
||||
style: FaiTheme.mono(
|
||||
size: 11,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (available && status.releaseNotesUrl != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'notes: ${status.releaseNotesUrl}',
|
||||
style: FaiTheme.mono(
|
||||
size: 11,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
FaiPill(
|
||||
label: available ? 'new' : 'offline',
|
||||
tone: available ? FaiPillTone.success : FaiPillTone.neutral,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue