v0.3: dashboard local mode, openbridge tab reuse, tab activation fix

- dashboard.html: local dashboard.json mode (10s poll) alongside artifact db mode;
  clickable cards behind an http(s)-only safeUrl filter; meta charset; mode-aware
  freshness line
- openbridge.py: localhost shim exposing open_link so dashboard clicks reuse the
  platform's warm tab instead of spawning one per click
- cdp.py: Tab.activate(); get_or_open_tab activates reused tabs — a backgrounded
  tab never finishes rendering, which made YouTube's login check read a signed-in
  account as signed out
- README/MANIFEST: corrected CDP port (9336, was documented as 9222), removed the
  false claim that the server writes dashboard.json, documented the reboot/VPS
  requirements
- .gitignore: __pycache__/, *.pyc, state.json, Archived/; removed stale tracked
  pycache and state.json from index
This commit is contained in:
2026-09-11 10:18:38 +08:00
parent eaedf4f409
commit 1ac06c1bbe
20 changed files with 719 additions and 72 deletions
+18 -1
View File
@@ -176,9 +176,14 @@ class CDPClient:
async def get_or_open_tab(self, url, url_contains):
"""Reuse a warm existing tab if one matches, otherwise open a fresh one.
Returns (tab, reused: bool). Caller should skip tab.close() when reused."""
Returns (tab, reused: bool). Caller should skip tab.close() when reused.
A reused tab is brought to the front — see Tab.activate() for why a
backgrounded tab cannot be trusted to have rendered.
"""
try:
tab = await self.attach_existing(url_contains)
await tab.activate()
return tab, True
except CDPError:
tab = await self.open_tab(url)
@@ -205,6 +210,18 @@ class Tab:
await self.client.send("Target.closeTarget", {"targetId": self.target_id})
self.client._drop_session(self.session_id)
async def activate(self):
"""Bring this tab to the front.
Not cosmetic. A backgrounded tab is not guaranteed to finish rendering:
YouTube leaves its masthead as `#masthead-skeleton-icons` with an empty
`#buttons` and never mounts `#avatar-btn`, so a login check that looks
for a rendered element reads a signed-in account as signed out — and
waiting does not help (measured 2026-09-11: still absent after 23s,
while ytcfg LOGGED_IN was true the whole time).
"""
await self.client.send("Target.activateTarget", {"targetId": self.target_id})
# ---- navigation ----------------------------------------------------
async def navigate(self, url, timeout=DEFAULT_TIMEOUT):