What it does
~/Library/Application Support/SkyRun/session_keep_alive.py runs every 10 min via launchd com.skyrun.session-keep-alive. For each chrome_bridge source (Track, KeyData, SmartLead, BeenVerified), it:
1. Finds the open Chrome tab matching the source URL
2. Reads page text + URL via chrome_bridge JS
3. Detects state: alive / aging / expired / no_tab / unknown
4. Writes per-source status to pwa/source_health.json with timestamp + last_alive_at
5. On flip from alive → expired: ntfy push w/ deep link to /preview-sources
Why it matters
Three benefits, in priority order:
1. Detection latency drops from hours → minutes. Before keep-alive, an expired session was only caught when an extractor tried to use it (hourly cadence). Now any expiration surfaces within 10 min, often paired with an ntfy push to Joseph's phone.
2. Probe traffic resets idle timers. Most sites kill sessions after N minutes of inactivity. The 10-min probe = constant low-volume activity = sessions live longer organically.
3. Real-time PWA Sources page. Each source card shows "Live" row with green/yellow/red dot pulsing per status, last-probed timestamp, and proper diagnosis copy. Joseph sees state at a glance, not after the next scheduled extract has tried + failed.
Status taxonomy
| Status | Meaning | Action |
|---|---|---|
alive | Probe found alive signals (e.g. "occupancy", "campaigns") in page text | None — probe again in 10 min |
aging | No login content but no alive signals either; inconclusive | Watch — could be a transient page (settings, profile) |
expired | Login URL or login-form content detected | ntfy push fires; tile shows ⚠ Reconnect; auto-detect flow takes over |
no_tab | No Chrome tab matches url_contains | Operator must open the URL once; future probes verify |
unknown | chrome_bridge unreachable / probe error | Treated as warn; surfaces in heartbeat errors |
Source-specific signals
Each source has its own login + alive heuristics defined in SOURCES array in session_keep_alive.py:
- Track:
url_contains: trackhs.com· login: "log in to get started" / "email address" / "forgot password" · alive: "check-ins" / "occupancy" / "front desk" - KeyData:
url_contains: keydatadashboard.com· login: "sign in" / "remember me" · alive: "adr" / "occupancy" / "revpar" / "your portfolio" - SmartLead:
url_contains: smartlead.ai· login: "sign in to your account" · alive: "campaigns" / "leads" / "smartlead" - BeenVerified:
url_contains: beenverified.com· login: "sign in" / "create an account" · alive: "dashboard" / "search" / "reports"
Wiring
- Local poller:
session_keep_alive.py(writespwa/source_health.json+ heartbeat) - launchd:
~/Library/LaunchAgents/com.skyrun.session-keep-alive.plist(StartInterval=600, RunAtLoad=true) - Trigger source allowlist:
_worker.jsacceptssource: "session_keep_alive"for manual probe-now - Extract trigger runner: routes
session_keep_alivesource tosession_keep_alive.py - Deploy script: copies
source_health.jsonintopwa-deploy/ - PWA Sources page: reads
/source_health.jsonon load, renders prober banner + per-source live row - PWA "Probe now" button: posts
/api/trigger-extractwith sourcesession_keep_alive
Future hardening
Possible additional layers:
1. Tier-2 keep-alive via real navigation: instead of just reading current URL, navigate the tab to the auth-protected page. Stronger signal but heavier.
2. API-token migration where supported: Track + KeyData both have REST APIs that survive browser session expiration. Adopting tokens would eliminate the keep-alive need for those sources entirely. Joseph would need to provision API keys.
3. Auto-launch Chrome with persistent profile: launchd watchdog to ensure Chrome is always running with the SkyRun profile, with --keep-alive flag.
4. Session resurrection from dumped cookies: dump cookies on each successful probe to disk; on detected expiry, attempt to restore the cookie and re-probe before declaring expired.
Heartbeat schema
Per-run heartbeat at health/<DATE>_session-keep-alive_<HHMM>.json:
json
{
"task_id": "session-keep-alive",
"status": "ok|partial|error",
"started_at": "...",
"completed_at": "...",
"summary": "chrome_bridge_avail=true · alive=3 aging=0 expired=0 no_tab=1 unknown=0 · 0 new expirations, 0 new revivals",
"warnings": [],
"errors": [],
"metrics": {
"alive_count": 3,
"aging_count": 0,
"expired_count": 0,
"no_tab_count": 1,
"unknown_count": 0,
"new_expirations": 0,
"new_revivals": 0
}
}