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:
flemming-it 2026-05-06 15:53:52 +02:00
parent 753db4fc60
commit 18db6ff164
5 changed files with 113 additions and 3 deletions

View file

@ -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;