From 3237c4415a58f0ae91509afa307f3d8984226727 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 28 May 2026 14:41:15 +0200 Subject: [PATCH] feat(doctor): per-source-kind capability breakdown line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` - `_sourceBreakdown(Map) -> String` renders the line in deterministic alphabetical order. dart analyze + flutter test green (11 integration tests). Signed-off-by: flemming-it --- lib/data/hub.dart | 15 +++++++++++++++ lib/pages/doctor.dart | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/data/hub.dart b/lib/data/hub.dart index e62399b..7e6211b 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -791,9 +791,19 @@ class HubService { // Module count = distinct module_name across capabilities. final moduleNames = {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 = {}; + for (final c in caps) { + final key = c.sourceKind.isEmpty ? c.kind : c.sourceKind; + bySource[key] = (bySource[key] ?? 0) + 1; + } + return DoctorSnapshot( moduleCount: moduleNames.length, capabilityCount: caps.length, + capabilitiesBySource: bySource, pendingApprovals: approvals.length, eventChainTotal: chain.total.toInt(), eventChainVerified: chain.verified.toInt(), @@ -862,6 +872,10 @@ class DoctorSnapshot { /// Filesystem paths the daemon owns. Empty fields when the /// hub couldn't resolve them (older daemon, no $HOME). 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 capabilitiesBySource; const DoctorSnapshot({ required this.moduleCount, @@ -873,6 +887,7 @@ class DoctorSnapshot { required this.services, required this.update, required this.paths, + this.capabilitiesBySource = const {}, }); bool get chainHealthy => eventChainTamperedAt == null; diff --git a/lib/pages/doctor.dart b/lib/pages/doctor.dart index 9306495..6ebd52d 100644 --- a/lib/pages/doctor.dart +++ b/lib/pages/doctor.dart @@ -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 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 { final DoctorSnapshot snapshot; @@ -677,14 +686,29 @@ class _ModulesPanel extends StatelessWidget { color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: FaiSpace.sm), - Text( - l.doctorModulesPanelSummary( - snapshot.moduleCount, - snapshot.capabilityCount, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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( label: snapshot.moduleCount > 0 ? l.doctorPillLoaded