mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add DHCP scanner to frontend status displays
Add a DHCP scanner card to the Status screen and a DHCP row to the consolidated Status card on the Home screen, mirroring the existing mDNS/SSDP implementations. Also label DHCP-discovered device events as "DHCP" instead of the raw "Dhcp". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
91eb0f17ba
commit
4e975598f9
@@ -33,6 +33,7 @@ class DeviceEvent {
|
||||
'Arp' => 'ARP',
|
||||
'Mdns' => 'mDNS',
|
||||
'Ssdp' => 'SSDP/UPnP',
|
||||
'Dhcp' => 'DHCP',
|
||||
_ => scanner,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
class DhcpScannerStatus {
|
||||
final bool isListening;
|
||||
final double? listeningForSeconds;
|
||||
final int devicesSeen;
|
||||
final double? lastDeviceSeenSecondsAgo;
|
||||
|
||||
const DhcpScannerStatus({
|
||||
required this.isListening,
|
||||
this.listeningForSeconds,
|
||||
required this.devicesSeen,
|
||||
this.lastDeviceSeenSecondsAgo,
|
||||
});
|
||||
|
||||
factory DhcpScannerStatus.fromJson(Map<String, dynamic> json) {
|
||||
return DhcpScannerStatus(
|
||||
isListening: json['is_listening'] as bool,
|
||||
listeningForSeconds: (json['listening_for_seconds'] as num?)?.toDouble(),
|
||||
devicesSeen: (json['devices_seen'] as num).toInt(),
|
||||
lastDeviceSeenSecondsAgo: (json['last_device_seen_seconds_ago'] as num?)
|
||||
?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/arp_scanner_card.dart';
|
||||
import '../widgets/dhcp_scanner_card.dart';
|
||||
import '../widgets/mdns_scanner_card.dart';
|
||||
import '../widgets/ssdp_scanner_card.dart';
|
||||
|
||||
@@ -19,6 +20,8 @@ class StatusScreen extends StatelessWidget {
|
||||
const MdnsScannerCard(),
|
||||
const SizedBox(height: 8),
|
||||
const SsdpScannerCard(),
|
||||
const SizedBox(height: 8),
|
||||
const DhcpScannerCard(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:frontend/utils/backend_reachability.dart';
|
||||
import 'package:frontend/utils/pref_utils.dart';
|
||||
import '../model/arp_scanner_status.dart';
|
||||
import '../model/dhcp_scanner_status.dart';
|
||||
import '../model/mdns_scanner_status.dart';
|
||||
import '../model/ssdp_scanner_status.dart';
|
||||
import '../model/device.dart';
|
||||
@@ -322,6 +323,16 @@ class BackendAPI {
|
||||
return SsdpScannerStatus.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<DhcpScannerStatus> getDhcpScannerStatus({
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final response = await _dio.get(
|
||||
'/dhcp_scanner/status',
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return DhcpScannerStatus.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<({List<Notification> items, bool hasNextPage})> listNotifications(
|
||||
bool? isNew, {
|
||||
int page = 0,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/dhcp_scanner_status.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../utils/duration_formatter.dart';
|
||||
import '../utils/oott_api.dart';
|
||||
import 'scanner_status_card.dart';
|
||||
|
||||
class DhcpScannerCard extends StatelessWidget {
|
||||
const DhcpScannerCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScannerStatusCard<DhcpScannerStatus>(
|
||||
title: 'DHCP Scanner',
|
||||
fetch: ({cancelToken}) =>
|
||||
BackendAPI.instance.getDhcpScannerStatus(cancelToken: cancelToken),
|
||||
resolver: _resolve,
|
||||
);
|
||||
}
|
||||
|
||||
static ScannerStatus _resolve(
|
||||
BuildContext context,
|
||||
DhcpScannerStatus status,
|
||||
double elapsed,
|
||||
) {
|
||||
if (!status.isListening) {
|
||||
return (
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
label: 'Not yet started',
|
||||
sublabels: [],
|
||||
);
|
||||
}
|
||||
final sublabels = <String>[
|
||||
status.listeningForSeconds != null
|
||||
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices seen'
|
||||
: '${status.devicesSeen} devices seen',
|
||||
if (status.lastDeviceSeenSecondsAgo != null)
|
||||
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
||||
];
|
||||
return (
|
||||
color: Theme.of(context).extension<AppColorExtension>()!.success,
|
||||
label: 'Listening',
|
||||
sublabels: sublabels,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../model/arp_scanner_status.dart';
|
||||
import '../model/dhcp_scanner_status.dart';
|
||||
import '../model/mdns_scanner_status.dart';
|
||||
import '../model/ssdp_scanner_status.dart';
|
||||
import '../navigation.dart';
|
||||
@@ -26,6 +27,7 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
late final PolledValue<ArpScannerStatus> _arp;
|
||||
late final PolledValue<MdnsScannerStatus> _mdns;
|
||||
late final PolledValue<SsdpScannerStatus> _ssdp;
|
||||
late final PolledValue<DhcpScannerStatus> _dhcp;
|
||||
Timer? _tickTimer;
|
||||
|
||||
@override
|
||||
@@ -50,6 +52,12 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
pollInterval: const Duration(seconds: 5),
|
||||
staleErrorAfter: const Duration(seconds: 30),
|
||||
);
|
||||
_dhcp = PolledValue<DhcpScannerStatus>(
|
||||
fetch: ({cancelToken}) =>
|
||||
BackendAPI.instance.getDhcpScannerStatus(cancelToken: cancelToken),
|
||||
pollInterval: const Duration(seconds: 5),
|
||||
staleErrorAfter: const Duration(seconds: 30),
|
||||
);
|
||||
_tickTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
@@ -87,6 +95,7 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
_arp.pause();
|
||||
_mdns.pause();
|
||||
_ssdp.pause();
|
||||
_dhcp.pause();
|
||||
_tickTimer?.cancel();
|
||||
_tickTimer = null;
|
||||
}
|
||||
@@ -95,6 +104,7 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
_arp.resume();
|
||||
_mdns.resume();
|
||||
_ssdp.resume();
|
||||
_dhcp.resume();
|
||||
_tickTimer ??= Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
@@ -108,6 +118,7 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
_arp.dispose();
|
||||
_mdns.dispose();
|
||||
_ssdp.dispose();
|
||||
_dhcp.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -118,15 +129,18 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
_arp,
|
||||
_mdns,
|
||||
_ssdp,
|
||||
_dhcp,
|
||||
BackendReachability.instance,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final arpFreshness = effectiveFreshness(_arp);
|
||||
final mdnsFreshness = effectiveFreshness(_mdns);
|
||||
final ssdpFreshness = effectiveFreshness(_ssdp);
|
||||
final dhcpFreshness = effectiveFreshness(_dhcp);
|
||||
if (arpFreshness == PolledFreshness.initialLoading ||
|
||||
mdnsFreshness == PolledFreshness.initialLoading ||
|
||||
ssdpFreshness == PolledFreshness.initialLoading) {
|
||||
ssdpFreshness == PolledFreshness.initialLoading ||
|
||||
dhcpFreshness == PolledFreshness.initialLoading) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
@@ -138,6 +152,7 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
final (arpColor, arpText) = _resolveArp(context, arpFreshness);
|
||||
final (mdnsColor, mdnsText) = _resolveMdns(context, mdnsFreshness);
|
||||
final (ssdpColor, ssdpText) = _resolveSsdp(context, ssdpFreshness);
|
||||
final (dhcpColor, dhcpText) = _resolveDhcp(context, dhcpFreshness);
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -179,6 +194,15 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
_ssdp,
|
||||
ssdpFreshness,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_scannerRow(
|
||||
context,
|
||||
dhcpColor,
|
||||
'DHCP',
|
||||
dhcpText,
|
||||
_dhcp,
|
||||
dhcpFreshness,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -301,4 +325,29 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
}
|
||||
return (neutralColor, 'Not yet started');
|
||||
}
|
||||
|
||||
(Color, String) _resolveDhcp(
|
||||
BuildContext context,
|
||||
PolledFreshness freshness,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final successColor = theme.extension<AppColorExtension>()!.success;
|
||||
final neutralColor = theme.colorScheme.outline;
|
||||
if (freshness == PolledFreshness.error) {
|
||||
return (theme.colorScheme.error, 'Error');
|
||||
}
|
||||
final status = _dhcp.value!;
|
||||
final elapsed = DateTime.now()
|
||||
.difference(_dhcp.lastSuccessAt!)
|
||||
.inSeconds
|
||||
.toDouble();
|
||||
if (status.isListening) {
|
||||
if (status.lastDeviceSeenSecondsAgo != null) {
|
||||
final secs = status.lastDeviceSeenSecondsAgo! + elapsed;
|
||||
return (successColor, 'Last device seen ${formatSeconds(secs)} ago');
|
||||
}
|
||||
return (successColor, 'No devices seen yet');
|
||||
}
|
||||
return (neutralColor, 'Not yet started');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user