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(),
|
(_) => VerifyEventChainResponse(),
|
||||||
),
|
),
|
||||||
_client.listServices().catchError((_) => <DeclaredService>[]),
|
_client.listServices().catchError((_) => <DeclaredService>[]),
|
||||||
|
_client.checkUpdate().catchError(
|
||||||
|
(_) => CheckUpdateResponse(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final caps = results[0] as List<CapabilityEntry>;
|
final caps = results[0] as List<CapabilityEntry>;
|
||||||
final approvals = results[1] as List<PendingApprovalEntry>;
|
final approvals = results[1] as List<PendingApprovalEntry>;
|
||||||
final chain = results[2] as VerifyEventChainResponse;
|
final chain = results[2] as VerifyEventChainResponse;
|
||||||
final services = results[3] as List<DeclaredService>;
|
final services = results[3] as List<DeclaredService>;
|
||||||
|
final update = results[4] as CheckUpdateResponse;
|
||||||
|
|
||||||
// Module count = distinct module_name across capabilities.
|
// Module count = distinct module_name across capabilities.
|
||||||
final moduleNames = <String>{for (final c in caps) c.moduleName};
|
final moduleNames = <String>{for (final c in caps) c.moduleName};
|
||||||
|
|
@ -237,10 +241,40 @@ class HubService {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.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 {
|
class DoctorSnapshot {
|
||||||
final int moduleCount;
|
final int moduleCount;
|
||||||
final int capabilityCount;
|
final int capabilityCount;
|
||||||
|
|
@ -249,6 +283,7 @@ class DoctorSnapshot {
|
||||||
final int eventChainVerified;
|
final int eventChainVerified;
|
||||||
final String? eventChainTamperedAt;
|
final String? eventChainTamperedAt;
|
||||||
final List<ServiceEntry> services;
|
final List<ServiceEntry> services;
|
||||||
|
final UpdateStatus update;
|
||||||
|
|
||||||
const DoctorSnapshot({
|
const DoctorSnapshot({
|
||||||
required this.moduleCount,
|
required this.moduleCount,
|
||||||
|
|
@ -258,6 +293,7 @@ class DoctorSnapshot {
|
||||||
required this.eventChainVerified,
|
required this.eventChainVerified,
|
||||||
required this.eventChainTamperedAt,
|
required this.eventChainTamperedAt,
|
||||||
required this.services,
|
required this.services,
|
||||||
|
required this.update,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get chainHealthy => eventChainTamperedAt == null;
|
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
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// 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 {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,14 @@ class _DoctorPageState extends State<DoctorPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final s = snapshot.data!;
|
final s = snapshot.data!;
|
||||||
|
final showUpdate = s.update.updateAvailable ||
|
||||||
|
(!s.update.manifestReachable &&
|
||||||
|
s.update.localVersion.isNotEmpty);
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(FaiSpace.xl),
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||||
children: [
|
children: [
|
||||||
|
if (showUpdate) _UpdateBanner(status: s.update),
|
||||||
|
if (showUpdate) const SizedBox(height: FaiSpace.lg),
|
||||||
_SummaryStrip(snapshot: s),
|
_SummaryStrip(snapshot: s),
|
||||||
const SizedBox(height: FaiSpace.xl),
|
const SizedBox(height: FaiSpace.xl),
|
||||||
_Section(
|
_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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ packages:
|
||||||
path: "../fai_dart_sdk"
|
path: "../fai_dart_sdk"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.1.0"
|
version: "0.2.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.8.0
|
version: 0.9.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue