Skip to content

fix: resolve infinite loading spinner on Logs page#2126

Open
Fizmatik wants to merge 1 commit intohiddify:mainfrom
Fizmatik:fix/logs-infinite-loading
Open

fix: resolve infinite loading spinner on Logs page#2126
Fizmatik wants to merge 1 commit intohiddify:mainfrom
Fizmatik:fix/logs-infinite-loading

Conversation

@Fizmatik
Copy link
Copy Markdown

@Fizmatik Fizmatik commented Apr 9, 2026

Summary

The Logs page shows an infinite loading spinner and never displays any log entries. This regression was introduced when log watching was migrated from file-based polling to gRPC streams.

Root cause: BehaviorSubject was created without a seed value, so logController.stream never emitted an initial value. The UI stays in AsyncLoading state permanently, rendering SliverLoadingBodyPlaceholder (the spinner).

Changes

  • Seed BehaviorSubject with empty list — ensures the stream always has an initial value for subscribers
  • Yield current buffer immediately in watchLogs() so the UI transitions to AsyncData right away
  • Add file-based fallback when gRPC core is not initialized — polls the log file directly (1s interval), matching the previous working implementation. Automatically stops when gRPC becomes available.
  • Use List.of() at all emission points — prevents mutable list aliasing where the same list reference was shared between the stream, BehaviorSubject, and UI consumers
  • Set cancelOnError: false and remove explicit cancel from onError handler — transient gRPC errors no longer permanently kill log collection
  • Set proper Timestamp on file-sourced log entries to avoid displaying epoch dates
  • Reset file position and notify logController on clearLogs()

Comparison with previous (working) implementation

The old ffi_singbox_service.dart used Watcher to poll the log file and always yielded an initial value:

Stream<List<String>> watchLogs(String path) async* {
  yield await _readLogFile(File(path));  // immediate yield
  yield* Watcher(path, ...).events.asyncMap(...);
}

The new gRPC-based implementation omitted both the initial yield and any fallback, causing the regression.

Test plan

  • Open Logs page when VPN is disconnected — should show empty list, not spinner
  • Open Logs page when VPN is connected — should show live logs
  • Connect VPN while on Logs page — logs should start appearing
  • Use level filter dropdown — should filter correctly
  • Clear logs button — should clear the list
  • Verify on both desktop and mobile

The Logs page shows an infinite loading spinner because the gRPC-based
log stream never emits an initial value. This regression was introduced
when log watching was migrated from file-based polling to gRPC streams.

Root cause: BehaviorSubject without a seed value, combined with no
fallback when gRPC is unavailable, leaves the UI in AsyncLoading state
permanently.

Changes:
- Seed BehaviorSubject with empty list for immediate stream emission
- Yield current buffer immediately in watchLogs() so UI never hangs
- Add file-based fallback (polling) when core is not initialized,
  matching the behavior of the previous working implementation
- Use List.of() at all emission points to prevent mutable aliasing
- Set cancelOnError: false and remove explicit cancel from onError
  so transient gRPC errors don't permanently kill log collection
- Set proper Timestamp on file-sourced log entries
- Reset file position and notify logController on clearLogs()
Fizmatik pushed a commit to Fizmatik/hiddify-app that referenced this pull request Apr 16, 2026
- Submodule: switch to enable-route-rules-v2 (based on upstream/main,
  not old v3 branch) — fixes Android SetupOptions API mismatch
- Cherry-pick PR hiddify#2126: fix infinite loading spinner on Logs page
  (file fallback when gRPC unavailable, seeded BehaviorSubject,
  cancelOnError: false for stream resilience)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant