// 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'; 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(); /// Reconnect to a new endpoint. Useful if the user changes /// the hub URL at runtime (Phase 1+). Future reconnect(HubEndpoint endpoint) async { await _client.close(); _client = HubClient(endpoint: endpoint); } 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, ); } /// 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, }); }