Files

176 lines
6.0 KiB
Dart
Raw Permalink Normal View History

2026-07-27 14:42:23 +02:00
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<List<AggregatedNodeStatus>> 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<List<AggregatedNodeStatus>> 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,
);
},
);
}
}