import 'package:flutter/material.dart'; import '../../../core/models/models.dart'; import '../../../core/models/severity_mapping.dart'; import '../../../core/theme/status_colors.dart'; import '../../../core/theme/theme_x.dart'; import '../../../core/utils/relative_time.dart'; import '../../../core/widgets/empty_state.dart'; class _FeedEntry { const _FeedEntry({ required this.timestamp, required this.title, required this.subtitle, required this.severity, required this.icon, }); final DateTime timestamp; final String title; final String subtitle; final Severity severity; final IconData icon; } /// Merges backup executions and OS update records from [NodeInfo] into one /// reverse-chronological feed. There's no persisted scan-result log on the /// server to fold in here — only these two histories actually exist. class ActivityFeed extends StatelessWidget { const ActivityFeed({super.key, required this.node, this.maxEntries = 6}); final NodeInfo node; final int maxEntries; List<_FeedEntry> _buildEntries() { final entries = <_FeedEntry>[ for (final execution in node.backup.history) _FeedEntry( timestamp: execution.timestamp, title: execution.success ? 'Backup completed' : 'Backup failed', subtitle: execution.message.isEmpty ? '${execution.durationSeconds}s' : execution.message, severity: severityForSuccess(execution.success), icon: Icons.cloud_outlined, ), for (final update in node.updateHistory) _FeedEntry( timestamp: update.timestamp, title: update.success ? 'System update applied' : 'System update failed', subtitle: update.packages.isEmpty ? update.message : '${update.packages.length} package(s)${update.message.isEmpty ? '' : ' · ${update.message}'}', severity: severityForSuccess(update.success), icon: Icons.history_rounded, ), ]..sort((a, b) => b.timestamp.compareTo(a.timestamp)); return entries.take(maxEntries).toList(); } @override Widget build(BuildContext context) { final entries = _buildEntries(); if (entries.isEmpty) { return const EmptyState( icon: Icons.history_rounded, title: 'No activity yet', message: 'Backup runs and system updates will show up here.', ); } return Column( children: [ for (var i = 0; i < entries.length; i++) ...[ if (i > 0) const Divider(height: 1), _FeedRow(entry: entries[i]), ], ], ); } } class _FeedRow extends StatelessWidget { const _FeedRow({required this.entry}); final _FeedEntry entry; @override Widget build(BuildContext context) { final color = entry.severity.resolve(context.status, muted: context.colors.outline); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 26, height: 26, decoration: BoxDecoration( color: Color.alphaBlend(color.withValues(alpha: 0.14), context.colors.surface), borderRadius: BorderRadius.circular(7), ), child: Icon(entry.icon, size: 14, color: color), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(entry.title, style: context.text.labelLarge), Text( entry.subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: context.text.bodySmall, ), ], ), ), const SizedBox(width: 12), Text( formatRelative(entry.timestamp), style: context.dataStyles.numericTabular.copyWith(fontSize: 12, color: context.colors.outline), ), ], ), ); } }