feat(studio): live audit feed via streamEvents (poll stays as safety net)
Some checks failed
Security / Security check (push) Failing after 1s

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 <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-19 10:07:08 +02:00
parent f5073b397e
commit 9bd5ddf8d2

View file

@ -26,19 +26,59 @@ class _AuditPageState extends State<AuditPage> {
bool _initialLoaded = false; bool _initialLoaded = false;
Timer? _poller; 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<AuditEvent>? _eventSub;
Timer? _nudgeDebounce;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_refresh(); _refresh();
_poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh()); _poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh());
_subscribeLive();
} }
@override @override
void dispose() { void dispose() {
_poller?.cancel(); _poller?.cancel();
_nudgeDebounce?.cancel();
_eventSub?.cancel();
super.dispose(); 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<void> _onClearPressed() async { Future<void> _onClearPressed() async {
final l = AppLocalizations.of(context)!; final l = AppLocalizations.of(context)!;
final outcome = await _ClearAuditDialog.show(context); final outcome = await _ClearAuditDialog.show(context);