feat(ground-suite): add replay observability console
This commit is contained in:
1 parent
1d28db2983
commit
c06c293f49
33 files changed
+2636
No files matched your search
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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 telemetry = state.observedTelemetryKinds
|
||||
.map((kind) => kind.label)
|
||||
.join(', ');
|
||||
final identity = heartbeat == null
|
||||
? 'Not observed'
|
||||
: 'System ${heartbeat.systemId} / component ${heartbeat.componentId}';
|
||||
final identityDetail = heartbeat == null
|
||||
? 'Awaiting HEARTBEAT.'
|
||||
: 'Reported by last HEARTBEAT: '
|
||||
'${heartbeat.armed ? 'Armed' : 'Disarmed'} · ${heartbeat.vehicleState}';
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final horizontalPadding = constraints.maxWidth < 600 ? 16.0 : 24.0;
|
||||
final cardWidth = constraints.maxWidth < 640
|
||||
? constraints.maxWidth - (horizontalPadding * 2)
|
||||
: 280.0;
|
||||
|
||||
return ListView(
|
||||
padding: EdgeInsets.all(horizontalPadding),
|
||||
children: [
|
||||
Text('Overview', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'This workspace is receive-only. A link or replay never implies a '
|
||||
'safe, armed, or command-capable vehicle.',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
_SummaryCard(
|
||||
title: 'Source',
|
||||
value: state.sourceLabel,
|
||||
width: cardWidth,
|
||||
detail: state.isReplay
|
||||
? 'Deterministic replay; no physical vehicle is connected.'
|
||||
: 'Receive-only transport.',
|
||||
icon: Icons.visibility_outlined,
|
||||
),
|
||||
_SummaryCard(
|
||||
title: 'Vehicle identity',
|
||||
value: identity,
|
||||
detail: identityDetail,
|
||||
width: cardWidth,
|
||||
icon: Icons.memory_outlined,
|
||||
),
|
||||
_SummaryCard(
|
||||
title: 'Observed streams',
|
||||
value: telemetry.isEmpty ? 'None' : telemetry,
|
||||
detail: state.isReplay
|
||||
? '${state.events.length} replayed observation(s).'
|
||||
: '${state.events.length} observation(s).',
|
||||
width: cardWidth,
|
||||
icon: Icons.sensors_outlined,
|
||||
),
|
||||
_SummaryCard(
|
||||
title: 'Decoder accounting',
|
||||
value:
|
||||
'${state.parserDropCount} parser / ${state.semanticDropCount} semantic',
|
||||
detail: 'Counters are zero in the typed replay fixture.',
|
||||
width: cardWidth,
|
||||
icon: Icons.rule_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Configuration is unavailable',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Kaptan firmware does not yet accept inbound configuration '
|
||||
'authority. Ground Suite will not show upload, commit, arm, '
|
||||
'or shutdown controls until the firmware exposes reviewed '
|
||||
'authenticated ingress and safe activation.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
const _SummaryCard({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.detail,
|
||||
required this.icon,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String value;
|
||||
final String detail;
|
||||
final IconData icon;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon),
|
||||
const SizedBox(height: 16),
|
||||
Text(title, style: Theme.of(context).textTheme.labelLarge),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
Text(detail),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user