feat(doctor): per-source-kind capability breakdown line
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Studio's Doctor page _ModulesPanel now shows the source
breakdown directly under the module/capability count:
Modules: 6 loaded, 10 capabilities
6 bundle · 3 mcp · 1 system
Reads `CapabilityEntry.sourceKind` (Phase D wire field).
Pre-0.12 hubs return empty sourceKind; the breakdown panel
falls back to grouping by the legacy `kind` string so older
deployments still see a useful count.
Two data-layer additions:
- `DoctorSnapshot.capabilitiesBySource: Map<String, int>`
- `_sourceBreakdown(Map<String, int>) -> String` renders
the line in deterministic alphabetical order.
dart analyze + flutter test green (11 integration tests).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
ceed8aab02
commit
3237c4415a
2 changed files with 45 additions and 6 deletions
|
|
@ -791,9 +791,19 @@ class HubService {
|
||||||
// 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};
|
||||||
|
|
||||||
|
// Per-source-kind breakdown (0.12+). Pre-0.12 hubs return
|
||||||
|
// empty `source_kind` strings — group those under `kind`
|
||||||
|
// as a fallback so the doctor panel still shows a count.
|
||||||
|
final bySource = <String, int>{};
|
||||||
|
for (final c in caps) {
|
||||||
|
final key = c.sourceKind.isEmpty ? c.kind : c.sourceKind;
|
||||||
|
bySource[key] = (bySource[key] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return DoctorSnapshot(
|
return DoctorSnapshot(
|
||||||
moduleCount: moduleNames.length,
|
moduleCount: moduleNames.length,
|
||||||
capabilityCount: caps.length,
|
capabilityCount: caps.length,
|
||||||
|
capabilitiesBySource: bySource,
|
||||||
pendingApprovals: approvals.length,
|
pendingApprovals: approvals.length,
|
||||||
eventChainTotal: chain.total.toInt(),
|
eventChainTotal: chain.total.toInt(),
|
||||||
eventChainVerified: chain.verified.toInt(),
|
eventChainVerified: chain.verified.toInt(),
|
||||||
|
|
@ -862,6 +872,10 @@ class DoctorSnapshot {
|
||||||
/// Filesystem paths the daemon owns. Empty fields when the
|
/// Filesystem paths the daemon owns. Empty fields when the
|
||||||
/// hub couldn't resolve them (older daemon, no $HOME).
|
/// hub couldn't resolve them (older daemon, no $HOME).
|
||||||
final DaemonPathsSnapshot paths;
|
final DaemonPathsSnapshot paths;
|
||||||
|
/// Per-source-kind capability count: bundle / system / mcp /
|
||||||
|
/// n8n / temporal / webhook (0.12+). Pre-0.12 hubs return
|
||||||
|
/// an empty map and the panel falls back to a flat count.
|
||||||
|
final Map<String, int> capabilitiesBySource;
|
||||||
|
|
||||||
const DoctorSnapshot({
|
const DoctorSnapshot({
|
||||||
required this.moduleCount,
|
required this.moduleCount,
|
||||||
|
|
@ -873,6 +887,7 @@ class DoctorSnapshot {
|
||||||
required this.services,
|
required this.services,
|
||||||
required this.update,
|
required this.update,
|
||||||
required this.paths,
|
required this.paths,
|
||||||
|
this.capabilitiesBySource = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get chainHealthy => eventChainTamperedAt == null;
|
bool get chainHealthy => eventChainTamperedAt == null;
|
||||||
|
|
|
||||||
|
|
@ -656,6 +656,15 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format the per-source-kind capability count map as a
|
||||||
|
/// human-readable line like "6 bundle · 3 mcp · 1 system".
|
||||||
|
/// Sorted alphabetically for deterministic rendering.
|
||||||
|
String _sourceBreakdown(Map<String, int> bySource) {
|
||||||
|
final entries = bySource.entries.toList()
|
||||||
|
..sort((a, b) => a.key.compareTo(b.key));
|
||||||
|
return entries.map((e) => '${e.value} ${e.key}').join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
class _ModulesPanel extends StatelessWidget {
|
class _ModulesPanel extends StatelessWidget {
|
||||||
final DoctorSnapshot snapshot;
|
final DoctorSnapshot snapshot;
|
||||||
|
|
||||||
|
|
@ -677,14 +686,29 @@ class _ModulesPanel extends StatelessWidget {
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
Text(
|
Expanded(
|
||||||
l.doctorModulesPanelSummary(
|
child: Column(
|
||||||
snapshot.moduleCount,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
snapshot.capabilityCount,
|
children: [
|
||||||
|
Text(
|
||||||
|
l.doctorModulesPanelSummary(
|
||||||
|
snapshot.moduleCount,
|
||||||
|
snapshot.capabilityCount,
|
||||||
|
),
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
if (snapshot.capabilitiesBySource.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
_sourceBreakdown(snapshot.capabilitiesBySource),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: snapshot.moduleCount > 0
|
label: snapshot.moduleCount > 0
|
||||||
? l.doctorPillLoaded
|
? l.doctorPillLoaded
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue