From 9bd5ddf8d2000a3b50435eb40983a98c998d321f Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 10:07:08 +0200 Subject: [PATCH] feat(studio): live audit feed via streamEvents (poll stays as safety net) The Audit page subscribes to streamEvents and nudges an immediate (debounced) refresh on each new event, so the feed updates instantly instead of waiting up to 2 s. Reuses the proven _refresh() path, so list management + filtering + hash-chain ordering are unchanged. The 2 s poll remains the safety net: on a persistent stream error the page is still fresh within the interval; a clean stream close re-subscribes once after 3 s. Subscription + debounce cancelled on dispose. Signed-off-by: flemming-it --- lib/pages/audit.dart | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) 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);