// HubService — singleton wrapper around fai_dart_sdk's HubClient. // // Centralises the gRPC connection so each page just imports // `HubService.instance` instead of constructing its own client. // Methods return UI-friendly types so pages stay free of // protobuf imports. import 'package:fai_dart_sdk/fai_dart_sdk.dart'; import 'package:shared_preferences/shared_preferences.dart'; class HubService { HubService._(); static final HubService instance = HubService._(); HubClient _client = HubClient(); /// The endpoint string ("http://host:port") the connection /// targets. Shown in the navigation rail. String get endpointLabel => _client.endpoint.toString(); HubEndpoint get currentEndpoint => _client.endpoint; static const _kHostKey = 'hub.host'; static const _kPortKey = 'hub.port'; static const _kSecureKey = 'hub.secure'; /// Read persisted endpoint and reconnect if it differs from /// the default. Called once at app start. Future loadPersistedEndpoint() async { final prefs = await SharedPreferences.getInstance(); final host = prefs.getString(_kHostKey); final port = prefs.getInt(_kPortKey); final secure = prefs.getBool(_kSecureKey); if (host == null) return; final endpoint = HubEndpoint( host: host, port: port ?? 50051, secure: secure ?? false, ); if (endpoint.toString() != _client.endpoint.toString()) { await reconnect(endpoint); } } /// Reconnect to a new endpoint and persist for next launch. Future reconnect(HubEndpoint endpoint) async { await _client.close(); _client = HubClient(endpoint: endpoint); final prefs = await SharedPreferences.getInstance(); await prefs.setString(_kHostKey, endpoint.host); await prefs.setInt(_kPortKey, endpoint.port); await prefs.setBool(_kSecureKey, endpoint.secure); } Future healthy() => _client.healthy(); Future> listModules() async { final caps = await _client.listCapabilities(); // Group capabilities by module so Studio's UI maps a card // to a module rather than a capability. final byModule = >{}; for (final c in caps) { byModule.putIfAbsent(c.moduleName, () => []).add(c); } return byModule.entries.map((e) { final caps = e.value; return ModuleSummary( name: e.key, version: caps.first.moduleVersion, capabilities: caps .map((c) => '${c.capability}@${c.version}') .toList(), ); }).toList() ..sort((a, b) => a.name.compareTo(b.name)); } Future> recentEvents({ int limit = 50, List types = const [], }) async { final events = await _client.eventLog(limit: limit, types: types); return events .map( (e) => AuditEvent( eventId: e.eventId, timestamp: DateTime.tryParse(e.timestamp) ?? DateTime.now(), type: e.eventType, flowName: e.flowName.isEmpty ? null : e.flowName, stepId: e.stepId.isEmpty ? null : e.stepId, moduleName: e.moduleName.isEmpty ? null : e.moduleName, durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(), error: e.error.isEmpty ? null : e.error, ), ) .toList(); } Future> pendingApprovals() async { final entries = await _client.listApprovals(statuses: ['pending']); return entries .map( (e) => PendingApproval( id: e.approvalId, flowName: e.flowName, stepId: e.stepId, prompt: e.prompt, showPreview: e.payloadPreview.isEmpty ? null : e.payloadPreview, createdAt: DateTime.tryParse(e.createdAt) ?? DateTime.now(), expiresAt: e.expiresAt.isEmpty ? null : DateTime.tryParse(e.expiresAt), ), ) .toList(); } Future approve(String id, String reviewer) => _client.approve(approvalId: id, reviewer: reviewer); Future reject(String id, String reviewer, String reason) => _client.reject( approvalId: id, reviewer: reviewer, reason: reason, ); /// Composite "doctor" snapshot. One round-trip per piece, run /// in parallel so the page populates fast. Future doctor() async { final results = await Future.wait([ _client.listCapabilities().catchError((_) => []), _client.listApprovals(statuses: const ['pending']) .catchError((_) => []), _client.verifyEventChain().catchError( (_) => VerifyEventChainResponse(), ), _client.listServices().catchError((_) => []), ]); final caps = results[0] as List; final approvals = results[1] as List; final chain = results[2] as VerifyEventChainResponse; final services = results[3] as List; // Module count = distinct module_name across capabilities. final moduleNames = {for (final c in caps) c.moduleName}; return DoctorSnapshot( moduleCount: moduleNames.length, capabilityCount: caps.length, pendingApprovals: approvals.length, eventChainTotal: chain.total.toInt(), eventChainVerified: chain.verified.toInt(), eventChainTamperedAt: chain.tamperedAt.isEmpty ? null : chain.tamperedAt, services: services .map( (s) => ServiceEntry( name: s.name, endpoint: s.endpoint, tags: s.tags, ), ) .toList(), ); } } class DoctorSnapshot { final int moduleCount; final int capabilityCount; final int pendingApprovals; final int eventChainTotal; final int eventChainVerified; final String? eventChainTamperedAt; final List services; const DoctorSnapshot({ required this.moduleCount, required this.capabilityCount, required this.pendingApprovals, required this.eventChainTotal, required this.eventChainVerified, required this.eventChainTamperedAt, required this.services, }); bool get chainHealthy => eventChainTamperedAt == null; } class ServiceEntry { final String name; final String endpoint; final List tags; const ServiceEntry({ required this.name, required this.endpoint, required this.tags, }); } /// UI-side type, decoupled from the proto wire type so pages /// don't import protobuf packages. class ModuleSummary { final String name; final String version; final List capabilities; const ModuleSummary({ required this.name, required this.version, required this.capabilities, }); } class AuditEvent { final String eventId; final DateTime timestamp; final String type; final String? flowName; final String? stepId; final String? moduleName; final int? durationMs; final String? error; const AuditEvent({ required this.eventId, required this.timestamp, required this.type, this.flowName, this.stepId, this.moduleName, this.durationMs, this.error, }); } class PendingApproval { final String id; final String flowName; final String stepId; final String prompt; final String? showPreview; final DateTime createdAt; final DateTime? expiresAt; const PendingApproval({ required this.id, required this.flowName, required this.stepId, required this.prompt, this.showPreview, required this.createdAt, this.expiresAt, }); }