Skip to content

feat: auto-refresh Signal Feed every 120s #37

@michaelzwang13

Description

@michaelzwang13

Problem

Even once #36 makes loads instant, the feed is frozen at page-open time. A new PR, mention, or message won't show up until the user manually reloads. The user has $\geq$1 active background watcher (PR Watcher polls every 120s) — the Signal Feed should keep pace.

Goal

Backend background poller refreshes every user's signal-feed cache every 120s, mirroring the pr_watcher.py shape. Frontend setInterval re-polls on the same cadence and swaps in fresh data silently (no spinner, no loading flicker).

Approach

Backend poller (mirrors backend/app/services/pr_watcher.py)

  • New service: backend/app/services/feed_poller.py (~100 lines)
  • 120s asyncio loop with per-tick exception isolation
  • Each tick walks users with $\geq$1 connected service via CredentialStore
  • For each (user, service): call the shared fetcher from feed_fetchers.py (from perf: cache Signal Feed for instant loads #36), write to signal_feed_cache
  • Per-(user, service) try/except so a Slack failure doesn't kill that user's Gmail/GitHub refresh
  • Lifespan wiring in backend/app/main.py mirrors the existing PR_WATCHER_ENABLED env gate: FEED_POLLER_ENABLED=true (added to backend/.env.example)

Frontend interval

In app/src/pages/Agents.tsx, add a second effect alongside the mount fetch:

useEffect(() => {
  const id = setInterval(() => {
    Promise.allSettled([fetchSlackMessages, fetchGmailMessages, fetchGithubActivity])
      .then(/* reuse mount-effect setters, no setFeedLoading */)
  }, 120_000)
  return () => clearInterval(id)
}, [])

UX notes:

  • No spinner on tick — page must not flash loading state on the auto-refresh
  • React key stability — rows already keyed by item.id, swap is in-place
  • Gate the existing filter-reset effect (useEffect(() => { setGhCategory('all'); setGhRepo('all') }, [githubData])) so it only resets on initial population, not every tick — otherwise the user's pinned filter resets every 2 min

Files

  • NEW backend/app/services/feed_poller.py
  • NEW backend/tests/test_feed_poller.py (mirror test_pr_watcher.py)
  • MOD backend/app/main.py — lifespan wires the poller
  • MOD backend/.env.example — add FEED_POLLER_ENABLED=true
  • MOD app/src/pages/Agents.tsx — 120s interval effect + filter-reset gating

Acceptance

  • Backend logs show feed_poller: tick fired every 120s
  • On /agents, a new PR opened in a watched repo appears within one poll cycle without manual reload
  • No loading spinner or flicker on auto-tick
  • User's category/repo filter selection persists across ticks
  • FEED_POLLER_ENABLED=false disables the poller cleanly

Out of scope

  • Per-user activity gating of the poller (currently polls all users with creds — add last_seen_at gating when user count grows past a handful)
  • Push updates (SSE / WebSocket) — much bigger lift, 120s polling already within "doesn't have to be instantaneous" bar

Related

Parent: #36 (caching layer)

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