Skip to content

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

Merged
michaelzwang13 merged 1 commit into
mainfrom
feat/signal-feed-auto-refresh
May 28, 2026
Merged

feat: auto-refresh Signal Feed every 120s#40
michaelzwang13 merged 1 commit into
mainfrom
feat/signal-feed-auto-refresh

Conversation

@michaelzwang13
Copy link
Copy Markdown
Owner

Summary

Closes #37. Builds on the cache layer from #36 to keep the feed live without manual reload.

Backend

  • New FeedPoller asyncio task wired into the FastAPI lifespan alongside PRWatcher. Walks every credential row every 120s, calls the matching feed fetcher, overwrites the cache. Env-gated via FEED_POLLER_ENABLED; per-pair exception isolation; write-only against the cache (a read would short-circuit the refresh the poller exists to perform)
  • CredentialModel.list_active_services() — minimal (user_id, service) query as the poller's enumeration source
  • Lifespan teardown refactored to a list-of-tasks pattern with cancel-all-then-await-all ordering so the second worker can't get free ticks during shutdown
  • FEED_POLLER_ENABLED=true added to .env.example; tests set it false in conftest.py so TestClient(app) doesn't spin up a real loop

Frontend

  • Second useEffect in Agents.tsx fires setInterval(120_000) to re-pull from the (warm) cache. No feedLoading flicker, no filter wipe
  • Removed the standalone useEffect(() => setGhCategory/Repo('all'), [githubData]) — it would fire on every tick and reset the user's filter. Reset now lives at the call sites where the dataset shape genuinely changes (mount fetch, OAuth callback, disconnect)

Tests: 161 → 168 backend (+7 new in test_feed_poller.py: empty-creds no-op, write-each-service, unknown-service skipped, connected=False not cached, per-pair error isolation, lifecycle cancel, tick-crash-doesnt-kill-loop). Frontend 14/14 unchanged.

Test plan

  • arch -arm64 .venv/bin/python -m pytest — 168 backend tests pass
  • bun run lint && bun run build && bun run test — frontend clean
  • Open /agents, wait ~120s, watch DevTools Network for the three /gateway/* re-fetches. UI updates silently (no spinner)
  • Apply a GitHub filter (e.g. category=pr), wait through a tick, confirm the filter is still applied
  • Backend logs show feed_poller: refreshed N cache entries every 120s
  • Ctrl-C the backend — clean shutdown, both workers log "shutting down"

🤖 Generated with Claude Code

Backend: new FeedPoller asyncio task wakes every 120s, walks every
(user, service) credential row, and overwrites the cache with the
result of the matching fetcher. Mirrors pr_watcher's shape — env-gated
via FEED_POLLER_ENABLED, per-pair exception isolation, write-only
against the cache (reads would defeat its purpose). Adds
CredentialModel.list_active_services() as the enumeration source.

Frontend: a second useEffect in Agents.tsx fires setInterval(120_000)
to re-pull all three feeds from the (now warm) cache. No feedLoading
flicker; filter selections persist across the swap so a refresh
doesn't strand the user on an empty 'all'. The standalone filter-reset
effect moved to specific call sites where the dataset shape genuinely
changes (mount fetch, OAuth callback, disconnect).

Closes #37.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@michaelzwang13
Copy link
Copy Markdown
Owner Author

@claude review

Comment on lines +38 to +40


class FeedPoller:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md docs convention violationFeedPoller is a peer to PRWatcher but is not documented in CLAUDE.md's Architecture section.

CLAUDE.md L25–27 documents the PR watcher service (its behavior, env gate, and file path). The FeedPoller is an equivalent background worker wired into the same lifespan, but the Architecture section has no entry for it.

CLAUDE.md rule: "When behavior or setup changes, update the relevant md (README.md, LOCAL_SETUP.md, ROADMAP.md, this file) in the same change."

The PR updates backend/.env.example with FEED_POLLER_ENABLED (correct), but CLAUDE.md itself needs a matching architecture bullet, e.g.:

Signal Feed poller. FastAPI lifespan starts an asyncio poll loop (backend/app/services/feed_poller.py) that walks every credential row every 120s, calls the matching feed fetcher (Slack/Gmail/GitHub), and writes results to the in-memory cache. Gated by FEED_POLLER_ENABLED.

@michaelzwang13 michaelzwang13 merged commit 8c41136 into main May 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: auto-refresh Signal Feed every 120s

1 participant