feat(doctor): per-source-kind capability breakdown line
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:
flemming-it 2026-05-28 14:41:15 +02:00
parent ceed8aab02
commit 3237c4415a
2 changed files with 45 additions and 6 deletions

View file

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

View file

@ -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,6 +686,10 @@ class _ModulesPanel extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: FaiSpace.sm), const SizedBox(width: FaiSpace.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
l.doctorModulesPanelSummary( l.doctorModulesPanelSummary(
snapshot.moduleCount, snapshot.moduleCount,
@ -684,7 +697,18 @@ class _ModulesPanel extends StatelessWidget {
), ),
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
const Spacer(), if (snapshot.capabilitiesBySource.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
_sourceBreakdown(snapshot.capabilitiesBySource),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
],
),
),
FaiPill( FaiPill(
label: snapshot.moduleCount > 0 label: snapshot.moduleCount > 0
? l.doctorPillLoaded ? l.doctorPillLoaded