import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/models/models.dart'; import '../../core/models/severity_mapping.dart'; import '../../core/network/api_exception.dart'; import '../../core/theme/status_colors.dart'; import '../../core/theme/theme_x.dart'; import '../../core/utils/relative_time.dart'; import '../../core/widgets/error_banner.dart'; import '../../core/widgets/stat_tile.dart'; import '../../data/repositories/nodes_repository.dart'; import 'providers/dashboard_summary.dart'; import 'providers/dashboard_summary_provider.dart'; import 'widgets/activity_feed.dart'; import 'widgets/quick_actions_row.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final summaryAsync = ref.watch(dashboardSummaryProvider); final fleetAsync = ref.watch(aggregatedNodesProvider); return summaryAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stackTrace) => Center( child: ErrorBanner( message: error is ApiException ? error.userMessage : error.toString(), onRetry: () => ref.invalidate(dashboardSummaryProvider), ), ), data: (summary) => ListView( padding: const EdgeInsets.all(24), children: [ _StatRow(summary: summary, fleetAsync: fleetAsync), const SizedBox(height: 22), Text('QUICK ACTIONS', style: context.text.titleSmall), const SizedBox(height: 10), const QuickActionsRow(), const SizedBox(height: 22), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('RECENT ACTIVITY', style: context.text.titleSmall), ], ), const SizedBox(height: 10), Card(child: ActivityFeed(node: summary.node)), ], ), ); } } class _StatRow extends StatelessWidget { const _StatRow({required this.summary, required this.fleetAsync}); final DashboardSummary summary; final AsyncValue> fleetAsync; @override Widget build(BuildContext context) { final services = summary.services; final up = services.where((s) => s.status == 'up').length; final down = services.where((s) => s.status == 'down').length; final unknown = services.length - up - down; final servicesSeverity = down > 0 ? Severity.critical : unknown > 0 ? Severity.warning : Severity.good; final servicesCaption = services.isEmpty ? 'No services discovered yet' : [ if (down > 0) '$down down', if (unknown > 0) '$unknown unknown', ].isEmpty ? 'All running' : [if (down > 0) '$down down', if (unknown > 0) '$unknown unknown'].join(' · '); final history = [...summary.node.backup.history] ..sort((a, b) => b.timestamp.compareTo(a.timestamp)); final lastBackup = history.isEmpty ? null : history.first; final updates = [...summary.node.updateHistory]..sort((a, b) => b.timestamp.compareTo(a.timestamp)); final lastUpdate = updates.isEmpty ? null : updates.first; return Wrap( spacing: 12, runSpacing: 12, children: [ SizedBox( width: 260, child: StatTile( label: 'Services', value: '$up', valueQualifier: '/ ${services.length} up', caption: servicesCaption, severity: services.isEmpty ? Severity.neutral : servicesSeverity, ), ), SizedBox( width: 260, child: StatTile( label: 'Last backup', value: lastBackup == null ? 'Never' : formatRelative(lastBackup.timestamp), caption: lastBackup == null ? 'No backups run yet' : (lastBackup.success ? '${lastBackup.durationSeconds}s' : lastBackup.message), severity: lastBackup == null ? Severity.neutral : severityForSuccess(lastBackup.success), ), ), SizedBox( width: 260, child: StatTile( label: 'Last system update', value: lastUpdate == null ? 'Never' : formatRelative(lastUpdate.timestamp), caption: lastUpdate == null ? 'No updates recorded yet' : '${lastUpdate.packages.length} package(s)', severity: lastUpdate == null ? Severity.neutral : severityForSuccess(lastUpdate.success), ), ), SizedBox(width: 260, child: _FleetTile(fleetAsync: fleetAsync)), ], ); } } class _FleetTile extends StatelessWidget { const _FleetTile({required this.fleetAsync}); final AsyncValue> fleetAsync; @override Widget build(BuildContext context) { return fleetAsync.when( loading: () => const StatTile( label: 'Fleet', value: '…', severity: Severity.neutral, ), error: (error, stackTrace) => StatTile( label: 'Fleet', value: '—', caption: error is ApiException ? error.userMessage : error.toString(), severity: Severity.critical, ), data: (nodes) { if (nodes.isEmpty) { return const StatTile( label: 'Fleet', value: '0', caption: 'No nodes registered', severity: Severity.neutral, ); } final online = nodes.where((n) => n.error == null).length; final unreachable = nodes.where((n) => n.error != null).toList(); return StatTile( label: 'Fleet', value: '$online', valueQualifier: '/ ${nodes.length} online', caption: unreachable.isEmpty ? 'All nodes reachable' : 'unreachable: ${unreachable.map((n) => n.node.name).join(', ')}', severity: unreachable.isEmpty ? Severity.good : Severity.critical, ); }, ); } }