import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/observations/observed_event.dart'; import '../../../shared/widgets/surfaces.dart'; import '../../observability/presentation/observability_controller.dart'; class OverviewPage extends ConsumerWidget { const OverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(observabilityControllerProvider); final heartbeat = state.lastHeartbeat; final observedStreams = state.observedTelemetryKinds; return LayoutBuilder( builder: (context, constraints) { final padding = constraints.maxWidth < 600 ? 16.0 : 28.0; final columns = constraints.maxWidth >= 1120 ? 3 : constraints.maxWidth >= 700 ? 2 : 1; final panelWidth = (constraints.maxWidth - (padding * 2) - (16 * (columns - 1))) / columns; return ListView( padding: EdgeInsets.all(padding), children: [ const ConsoleLabel('Vehicle observation'), const SizedBox(height: 8), Text( 'Replay session', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 6), Text( 'Read-only observations from the selected source.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), Wrap( spacing: 16, runSpacing: 16, children: [ _MetricPanel( width: panelWidth, label: 'Replay session', value: state.sourceLabel, accent: Theme.of(context).colorScheme.secondary, child: const Text( 'Deterministic fixture. No physical vehicle is connected.', ), ), _MetricPanel( width: panelWidth, label: 'Last reported heartbeat', value: heartbeat == null ? 'Awaiting heartbeat' : 'SYS ${heartbeat.systemId} · COMP ${heartbeat.componentId}', monoValue: true, child: _HeartbeatFacts(heartbeat: heartbeat), ), _MetricPanel( width: panelWidth, label: 'Ingress accounting', value: '${state.parserDropCount} / ${state.semanticDropCount}', monoValue: true, child: Text( state.isReplay ? 'Parser / semantic drops. Not evaluated for typed replay.' : 'Parser / semantic drops from received frames.', ), ), ], ), const SizedBox(height: 16), InstrumentPanel( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ConsoleLabel('Observed data streams'), const SizedBox(height: 16), Wrap( spacing: 10, runSpacing: 10, children: [ for (final kind in TelemetryKind.values) _StreamIndicator( kind: kind, observed: observedStreams.contains(kind), ), ], ), ], ), ), ), const SizedBox(height: 16), InstrumentPanel( accent: Theme.of(context).colorScheme.outline, child: const Padding( padding: EdgeInsets.all(20), child: _UnavailableSetup(), ), ), ], ); }, ); } } class _MetricPanel extends StatelessWidget { const _MetricPanel({ required this.width, required this.label, required this.value, required this.child, this.accent, this.monoValue = false, }); final double width; final String label; final String value; final Widget child; final Color? accent; final bool monoValue; @override Widget build(BuildContext context) => SizedBox( width: width, child: InstrumentPanel( accent: accent, child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ConsoleLabel(label), const SizedBox(height: 14), Text( value, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontFamily: monoValue ? 'monospace' : null, ), ), const SizedBox(height: 10), DefaultTextStyle.merge( style: Theme.of(context).textTheme.bodySmall, child: child, ), ], ), ), ), ); } class _HeartbeatFacts extends StatelessWidget { const _HeartbeatFacts({required this.heartbeat}); final HeartbeatEvent? heartbeat; @override Widget build(BuildContext context) { if (heartbeat == null) { return const Text('No heartbeat has been reported by this source.'); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _FactLine( label: 'Reported arm state', value: heartbeat!.armed ? 'ARMED' : 'DISARMED', ), const SizedBox(height: 4), _FactLine( label: 'Reported MAV state', value: heartbeat!.vehicleState.toUpperCase(), ), ], ); } } class _FactLine extends StatelessWidget { const _FactLine({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) => RichText( text: TextSpan( style: Theme.of(context).textTheme.bodySmall, children: [ TextSpan(text: '$label: '), TextSpan( text: value, style: const TextStyle( fontFamily: 'monospace', fontWeight: FontWeight.w700, ), ), ], ), ); } class _StreamIndicator extends StatelessWidget { const _StreamIndicator({required this.kind, required this.observed}); final TelemetryKind kind; final bool observed; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final tone = observed ? scheme.primary : scheme.onSurfaceVariant; return DecoratedBox( decoration: BoxDecoration( color: tone.withValues(alpha: observed ? 0.12 : 0.06), border: Border.all( color: tone.withValues(alpha: observed ? 0.5 : 0.32), ), borderRadius: BorderRadius.circular(8), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon( observed ? Icons.check_circle_outline : Icons.remove_circle_outline, size: 17, color: tone, ), const SizedBox(width: 7), Text(kind.label), const SizedBox(width: 7), Text( observed ? 'Observed' : 'Not observed', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ); } } class _UnavailableSetup extends StatelessWidget { const _UnavailableSetup(); @override Widget build(BuildContext context) => Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.lock_outline), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ConsoleLabel('Vehicle setup · unavailable'), const SizedBox(height: 8), Text('This firmware accepts no inbound MAVLink authority.'), ], ), ), ], ); }