diff --git a/lib/pages/audit.dart b/lib/pages/audit.dart index 07dd7cf..fdf4d66 100644 --- a/lib/pages/audit.dart +++ b/lib/pages/audit.dart @@ -26,19 +26,59 @@ class _AuditPageState extends State { bool _initialLoaded = false; Timer? _poller; + /// Live event feed. New events nudge an immediate refresh so the + /// page updates instantly instead of waiting up to 2 s. The poll + /// stays as the safety net: if the stream errors or the hub drops, + /// the page is still fresh within the poll interval. Reusing the + /// proven `_refresh()` (re-fetch latest 100) keeps list management, + /// filtering, and the hash-chain ordering exactly as before. + StreamSubscription? _eventSub; + Timer? _nudgeDebounce; + @override void initState() { super.initState(); _refresh(); _poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh()); + _subscribeLive(); } @override void dispose() { _poller?.cancel(); + _nudgeDebounce?.cancel(); + _eventSub?.cancel(); super.dispose(); } + void _subscribeLive() { + _eventSub?.cancel(); + _eventSub = HubService.instance.streamEvents(backfill: 0).listen( + (_) => _nudge(), + // On a persistent error the 2 s poll keeps the page fresh, so we + // do nothing. On a clean close (hub restart / stream end) try to + // re-establish the live feed once after a short delay. + onError: (_) {}, + onDone: () { + _eventSub = null; + if (mounted) { + Timer(const Duration(seconds: 3), () { + if (mounted && _eventSub == null) _subscribeLive(); + }); + } + }, + cancelOnError: true, + ); + } + + /// Coalesce a burst of events into a single refresh. + void _nudge() { + if (_nudgeDebounce?.isActive ?? false) return; + _nudgeDebounce = Timer(const Duration(milliseconds: 300), () { + if (mounted) _refresh(); + }); + } + Future _onClearPressed() async { final l = AppLocalizations.of(context)!; final outcome = await _ClearAuditDialog.show(context);