mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
The Request Logger gated each auto-refresh tick on a static document.visibilityState === "visible" read. Hosts that report a permanent non-"visible" state without ever firing a visibilitychange event (Docker dashboard wrappers, embedded/proxied webviews) froze auto-refresh entirely — only the manual Refresh button worked, a regression from 3.8.24's unconditional polling. The pause is now event-driven and fail-open: visibleRef starts true and is only flipped to false on a real visibilitychange → hidden transition, so a host that never signals a genuine background transition keeps polling, while normal browser tabs still pause when actually backgrounded. Regression test reproduces the misreporting-host case (RED) and the perf guard is re-encoded under the event-driven semantics.
This commit is contained in:
committed by
GitHub
parent
c89adad0b4
commit
4941a0d462
@@ -15,6 +15,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(ws): start the LiveWS sidecar with `cwd` at the package root (global/systemd installs)** — the standalone LiveWS launcher (`scripts/start-ws-server.mjs`) re-spawns itself with `node --import tsx <self>` but did not set `cwd`. When the WebSocket sidecar was launched from outside the package directory — a global npm/homebrew install, or a `systemd`/`launchd` unit started from `$HOME` — Node could not resolve the `tsx` package (`ERR_MODULE_NOT_FOUND: Cannot find package 'tsx'`), and even from the package directory `tsx` could not resolve the tsconfig `@/*` path aliases (e.g. `@/types/databaseSettings`), so the sidecar never booted. The spawn now pins `cwd` to the package root (the directory above `scripts/`, where `package.json` + `tsconfig.json` live), which resolves both `tsx` discovery and the `@/*` aliases regardless of launch directory. ([#4055](https://github.com/diegosouzapw/OmniRoute/issues/4055) — thanks @Rahulsharma0810)
|
||||
- **fix(dashboard): Logs page auto-refresh now works in embedded/proxied dashboards** — the Request Logger gated each auto-refresh tick on a static `document.visibilityState === "visible"` read. Hosts that report a permanent non-`"visible"` state without ever firing a `visibilitychange` event (Docker dashboard wrappers, embedded webviews) froze auto-refresh entirely — only the manual Refresh button worked, a regression from 3.8.24's unconditional polling. The pause is now event-driven and fail-open: polling starts enabled and only pauses after a real `visibilitychange` → hidden transition (still preserving the backgrounded-tab optimization for normal browser tabs). ([#4054](https://github.com/diegosouzapw/OmniRoute/issues/4054) — thanks @tjengbudi)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -28,11 +28,7 @@ import {
|
||||
} from "@/shared/utils/formatting";
|
||||
import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
|
||||
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
|
||||
import {
|
||||
computeLogsSignature,
|
||||
resolveInitialVisibility,
|
||||
shouldAutoRefresh,
|
||||
} from "./requestLoggerSignature";
|
||||
import { computeLogsSignature, shouldAutoRefresh } from "./requestLoggerSignature";
|
||||
import {
|
||||
DEFAULT_REFRESH_INTERVAL_SEC,
|
||||
clampRefreshIntervalSec,
|
||||
@@ -153,7 +149,12 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
const scrollContainerRef = useRef(null);
|
||||
const loadMoreSentinelRef = useRef(null);
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
const visibleRef = useRef(resolveInitialVisibility());
|
||||
// #4054: fail-open. The auto-refresh pause is event-driven — we start assuming
|
||||
// the tab is visible (poll) and only flip to paused on a real `visibilitychange`
|
||||
// → hidden transition. Seeding from a static `document.visibilityState` read froze
|
||||
// polling forever in embedded/proxied hosts that report a permanent non-"visible"
|
||||
// state without ever dispatching the event (Docker dashboard wrappers, webviews).
|
||||
const visibleRef = useRef(true);
|
||||
|
||||
// Column visibility with localStorage persistence
|
||||
const [visibleColumns, setVisibleColumns] = useState(() => {
|
||||
@@ -291,10 +292,13 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
if (!selectedLog && shouldAutoRefresh(recording, limit, PAGE_SIZE)) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
// #3972: read live visibility each tick, not a mount-time ref. A tab
|
||||
// mounted while "hidden" with no later `visibilitychange` left the ref
|
||||
// false forever, so auto-refresh never ran (only the manual button did).
|
||||
if (typeof document === "undefined" || document.visibilityState === "visible") {
|
||||
// #3972/#4054: gate on the event-tracked `visibleRef` (fail-open), not a
|
||||
// static `document.visibilityState` read. A real browser tab that goes to
|
||||
// the background fires `visibilitychange` → `visibleRef=false` → we pause
|
||||
// (perf). An embedded/proxied host that misreports a permanent "hidden"
|
||||
// state never fires the event, so `visibleRef` stays `true` and polling
|
||||
// keeps working instead of freezing forever.
|
||||
if (visibleRef.current) {
|
||||
fetchLogs(false);
|
||||
}
|
||||
}, refreshIntervalSec * 1000);
|
||||
|
||||
@@ -95,9 +95,9 @@ afterEach(async () => {
|
||||
setVisibility("visible");
|
||||
});
|
||||
|
||||
describe("RequestLoggerV2 auto-refresh (#3972)", () => {
|
||||
describe("RequestLoggerV2 auto-refresh (#3972 + #4054)", () => {
|
||||
it("keeps polling on the interval when the tab becomes visible without a visibilitychange event", async () => {
|
||||
// Mounts while the document reports "hidden" → resolveInitialVisibility() = false.
|
||||
// Mounts while the document reports "hidden", no visibilitychange ever fires.
|
||||
setVisibility("hidden");
|
||||
|
||||
await act(async () => {
|
||||
@@ -123,7 +123,14 @@ describe("RequestLoggerV2 auto-refresh (#3972)", () => {
|
||||
expect(callLogsRequests).toBeGreaterThan(afterMount);
|
||||
});
|
||||
|
||||
it("does not poll while the tab stays hidden (preserves the hidden-tab optimization)", async () => {
|
||||
it("keeps polling when visibilityState is pinned 'hidden' but no visibilitychange ever fires (#4054)", async () => {
|
||||
// Embedded / proxied dashboard host (e.g. a Docker wrapper or webview) that
|
||||
// reports a permanent non-"visible" state and NEVER dispatches a
|
||||
// `visibilitychange` event. 3.8.24 polled unconditionally; the static-visibility
|
||||
// gate added since then froze auto-refresh in these hosts — only the manual
|
||||
// Refresh button worked, exactly as reported in #4054. The gate must be
|
||||
// fail-open: a host that never signals a *real* background transition keeps
|
||||
// polling.
|
||||
setVisibility("hidden");
|
||||
|
||||
await act(async () => {
|
||||
@@ -133,12 +140,40 @@ describe("RequestLoggerV2 auto-refresh (#3972)", () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
const afterMount = callLogsRequests;
|
||||
expect(afterMount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Stays hidden across two ticks → must not poll.
|
||||
// visibilityState stays "hidden", NO visibilitychange event is dispatched.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(DEFAULT_REFRESH_INTERVAL_SEC * 1000);
|
||||
});
|
||||
|
||||
expect(callLogsRequests).toBeGreaterThan(afterMount);
|
||||
});
|
||||
|
||||
it("pauses polling after a real visibilitychange → hidden (preserves the backgrounded-tab optimization)", async () => {
|
||||
// The perf guard is now keyed on the *event*, not the static value: a genuine
|
||||
// background transition fires `visibilitychange`, and only then do we pause.
|
||||
setVisibility("visible");
|
||||
|
||||
await act(async () => {
|
||||
root.render(<RequestLoggerV2 />);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
// Real background transition: the state flips AND the browser fires the event.
|
||||
setVisibility("hidden");
|
||||
await act(async () => {
|
||||
document.dispatchEvent(new Event("visibilitychange"));
|
||||
});
|
||||
const afterHidden = callLogsRequests;
|
||||
|
||||
// Stays backgrounded across two ticks → must not poll.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(DEFAULT_REFRESH_INTERVAL_SEC * 2000);
|
||||
});
|
||||
|
||||
expect(callLogsRequests).toBe(afterMount);
|
||||
expect(callLogsRequests).toBe(afterHidden);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user