Skip to content

dashboard: useSystemStatus stuck on "Linking modules..." forever if tab is hidden/backgrounded at initial load #330

@yasinBursali

Description

@yasinBursali

Summary

useSystemStatus (dream-server/extensions/services/dashboard/src/hooks/useSystemStatus.js) guards its polling loop with if (document.hidden) return — but that guard runs before the very first fetch, not only subsequent ones. If the dashboard tab is not focused/visible when useEffect fires (opened in a background tab, launched via MCP/Playwright automation, restored by the browser session, loaded on a secondary monitor while another tab is focused, etc.), the initial fetch is skipped, loading is never set to false, and the Dashboard page stays frozen on the loading skeleton forever:

Linking modules... reading telemetry...

with a sidebar reading "Services Online: 0/0".

The only escape hatch is the visibilitychange listener — so if the user eventually switches to the tab, it unblocks. But (a) a user who never switches (parked it on a second monitor, dragged it behind another window) will see an apparently-broken dashboard indefinitely, and (b) any automated tool that opens the tab without bringing it to the foreground (Playwright, Puppeteer, MCP Chrome, cron-screenshot services, etc.) will never get past this screen.

Reproduction

Method A (manual):

  1. Open Chrome.
  2. In a backgrounded Chrome window (e.g. minimize it, or cover it with another app), Cmd+Shift+T / middle-click a link to http://localhost:3001/ to open it as a background tab.
  3. Wait 10+ seconds. Leave focus elsewhere.
  4. Switch windows to reveal the tab without clicking on it.
  5. Observed: page stuck on "Linking modules... reading telemetry..." and "Services Online: 0/0", even though /api/status is healthy.
  6. Click on the page (fires visibilitychange) → data loads.

Method B (scripted — how I hit it during integration testing):

  1. From any MCP / Puppeteer / Playwright browser automation, open http://localhost:3001/ in a new tab without bringing it to the foreground:
    await page.goto('http://localhost:3001/')
    await page.evaluate(() => document.querySelector('main')?.innerText)
    // → 'Linking modules... reading telemetry...' (forever)
  2. No amount of waiting fixes it; document.hidden stays true for a backgrounded automation tab.
  3. Workaround that unblocks it:
    Object.defineProperty(document, 'hidden', {configurable: true, get: () => false});
    Object.defineProperty(document, 'visibilityState', {configurable: true, get: () => 'visible'});
    location.reload();

Code

`dream-server/extensions/services/dashboard/src/hooks/useSystemStatus.js` (relevant block, unchanged on upstream/main):

```js
useEffect(() => {
const fetchStatus = async () => {
if (USE_MOCK_DATA) {
setLoading(false)
return
}

// Pause polling when the tab is hidden to save CPU/network
if (document.hidden) return    // ← problem: also skips the INITIAL fetch

if (fetchInFlight.current) return
fetchInFlight.current = true

try {
  const response = await fetch('/api/status')
  if (!response.ok) throw new Error('Failed to fetch status')
  const data = await response.json()
  setStatus(data)
  setError(null)
} catch (err) {
  setError(err.message)
} finally {
  fetchInFlight.current = false
  setLoading(false)      // ← never runs on hidden tab
}

}

fetchStatus()
const interval = setInterval(fetchStatus, POLL_INTERVAL)

const onVisibility = () => { if (!document.hidden) fetchStatus() }
document.addEventListener('visibilitychange', onVisibility)
...
}, [])
```

Why the existing visibilitychange listener doesn't fully save it

It does fire when the tab transitions from hidden to visible. But:

  • If the tab is opened hidden and stays hidden (user never focuses it), the transition never happens.
  • The listener calls fetchStatus — which itself still has the if (document.hidden) return guard at the top. So if the listener somehow fires while document.hidden is still true (race between visibilitychange and the actual focus state), it no-ops.

Proposed fix

Track whether an initial fetch has succeeded, and only apply the hidden-tab skip for subsequent polls:

```js
const hasInitialData = useRef(false)

const fetchStatus = async () => {
if (USE_MOCK_DATA) { setLoading(false); return }

// Skip background polls — but always run the initial fetch so the
// UI leaves its loading skeleton even if the tab opened hidden.
if (document.hidden && hasInitialData.current) return

if (fetchInFlight.current) return
fetchInFlight.current = true

try {
const response = await fetch('/api/status')
if (!response.ok) throw new Error('Failed to fetch status')
const data = await response.json()
setStatus(data)
setError(null)
hasInitialData.current = true
} catch (err) {
setError(err.message)
} finally {
fetchInFlight.current = false
setLoading(false)
}
}
```

This preserves the original CPU/bandwidth savings for the common case (user has the dashboard in a background tab for hours, don't poll every 5s) while guaranteeing the dashboard always leaves the "Linking modules..." state. It also hardens the visibilitychange path — on first reveal, hasInitialData=false forces the fetch through regardless of any visibility race.

Optional defensive improvement: in the finally block, call setLoading(false) even on the "skipped because hidden" path, so the UI can render a sensible empty state instead of the loading skeleton. But the one-shot-initial-fetch approach above is simpler.

Severity

Low-to-medium UX bug. Real users who always focus their browser tabs will rarely hit it. But:

  • Multi-monitor users who park the dashboard on a secondary display without clicking it first → always hit it.
  • Browser automation (MCP Chrome, Playwright, Puppeteer, cron-based screenshotters) → always hit it.
  • Tab-restore on Chrome startup → often hit it (restored tabs are hidden until clicked).
  • Users who open from a dream-macos.sh open CLI command that launches Chrome in the background → always hit it.

Trust issue: anyone who reports "the dashboard is broken" to the maintainers when it's actually the tab-visibility bug will waste triage time. I hit this twice while validating PRs in Claude Code's MCP browser.

Not caused by any of the open PRs

Confirmed pre-existing. `useSystemStatus.js` is not in the file list of any of PRs Light-Heart-Labs#893Light-Heart-Labs#909. The bug exists on `Light-Heart-Labs/DreamServer main` unchanged.

Environment

  • macOS Darwin 25.2.0, Apple Silicon
  • Chrome (MCP-controlled tab + manual focused tab both reproduce)
  • Dashboard build: index-DjU9Rbg6.js (from integration install of upstream/main + 17 yasinBursali PRs)
  • Install at /Volumes/X/dream-server-test

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions