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):
- Open Chrome.
- 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.
- Wait 10+ seconds. Leave focus elsewhere.
- Switch windows to reveal the tab without clicking on it.
- Observed: page stuck on "Linking modules... reading telemetry..." and "Services Online: 0/0", even though
/api/status is healthy.
- Click on the page (fires
visibilitychange) → data loads.
Method B (scripted — how I hit it during integration testing):
- 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)
- No amount of waiting fixes it;
document.hidden stays true for a backgrounded automation tab.
- 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#893–Light-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
Summary
useSystemStatus(dream-server/extensions/services/dashboard/src/hooks/useSystemStatus.js) guards its polling loop withif (document.hidden) return— but that guard runs before the very first fetch, not only subsequent ones. If the dashboard tab is not focused/visible whenuseEffectfires (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,loadingis never set tofalse, and the Dashboard page stays frozen on the loading skeleton forever:with a sidebar reading "Services Online: 0/0".
The only escape hatch is the
visibilitychangelistener — 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):
Cmd+Shift+T/ middle-click a link tohttp://localhost:3001/to open it as a background tab./api/statusis healthy.visibilitychange) → data loads.Method B (scripted — how I hit it during integration testing):
http://localhost:3001/in a new tab without bringing it to the foreground:document.hiddenstaystruefor a backgrounded automation tab.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
}
}
fetchStatus()
const interval = setInterval(fetchStatus, POLL_INTERVAL)
const onVisibility = () => { if (!document.hidden) fetchStatus() }
document.addEventListener('visibilitychange', onVisibility)
...
}, [])
```
Why the existing
visibilitychangelistener doesn't fully save itIt does fire when the tab transitions from hidden to visible. But:
fetchStatus— which itself still has theif (document.hidden) returnguard at the top. So if the listener somehow fires whiledocument.hiddenis still true (race betweenvisibilitychangeand 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
visibilitychangepath — on first reveal,hasInitialData=falseforces the fetch through regardless of any visibility race.Optional defensive improvement: in the
finallyblock, callsetLoading(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:
dream-macos.sh openCLI 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#893–Light-Heart-Labs#909. The bug exists on `Light-Heart-Labs/DreamServer main` unchanged.
Environment
index-DjU9Rbg6.js(from integration install of upstream/main + 17 yasinBursali PRs)/Volumes/X/dream-server-test