feat(store,doctor): surface the hub's trust + exposure data (0.77.0)
Some checks failed
Security / Security check (push) Failing after 1s

The trust-gate dialog replaces its generic 'no per-entry status yet
(alpha)' note with the hub's classified verification statement —
pinned store key / trusted publishers (green), installs without
signature checking (amber), install would be refused (red), bridge
entry (neutral) — via one shared describeInstallVerification mapping.
Against a pre-0.23 hub the field is empty and the old honest wording
stays (pinned by test).

Information architecture: policy-off is a GLOBAL fact, so it appears
as ONE ChainInlineHelp notice above the store grid instead of a
warning pill on every card (card noise); only 'blocked' — a genuine
per-source anomaly — earns a card pill. Doctor's host services show
the hub-classified network reach per endpoint (local only / private
network / publicly reachable with a protect-it hint / reach unknown).

Verified end-to-end against the live dev hub (guide harness): the
wire field arrives, the store page shows exactly one policy notice
and quiet cards; trust-gate variants captured via the dialog
harness. Suite 123 green.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-21 13:14:13 +02:00
parent 87afa4dc05
commit 2f076ccf29
15 changed files with 732 additions and 13 deletions

View file

@ -1159,6 +1159,9 @@ class _ServicesPanel extends StatelessWidget {
),
),
const Spacer(),
_ServiceExposurePill(
exposure: snapshot.services[i].exposure,
),
for (final tag in snapshot.services[i].tags) ...[
ChainPill(label: tag, tone: ChainPillTone.neutral),
const SizedBox(width: ChainSpace.xs),
@ -1173,6 +1176,43 @@ class _ServicesPanel extends StatelessWidget {
}
}
/// Network-reach pill per declared service, from the hub's
/// endpoint classification (`DeclaredService.exposure`). Public
/// endpoints get the warning tone a host service reachable from
/// outside is the one thing an operator should notice here.
/// Empty (pre-0.23 hub) renders nothing.
class _ServiceExposurePill extends StatelessWidget {
final String exposure;
const _ServiceExposurePill({required this.exposure});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final (label, tone, hint) = switch (exposure) {
'loopback' => (l.svcExposureLoopback, ChainPillTone.neutral, ''),
'private' => (l.svcExposurePrivate, ChainPillTone.neutral, ''),
'public' => (
l.svcExposurePublic,
ChainPillTone.warning,
l.svcExposurePublicHint,
),
'unknown' => (
l.svcExposureUnknown,
ChainPillTone.neutral,
l.svcExposureUnknownHint,
),
_ => ('', ChainPillTone.neutral, ''),
};
if (label.isEmpty) return const SizedBox.shrink();
final pill = ChainPill(label: label, tone: tone);
return Padding(
padding: const EdgeInsets.only(right: ChainSpace.xs),
child: hint.isEmpty ? pill : Tooltip(message: hint, child: pill),
);
}
}
class _UpdateBanner extends StatefulWidget {
final UpdateStatus status;