feat: auto-refresh Signal Feed every 120s#40
Conversation
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>
|
@claude review |
|
|
||
|
|
||
| class FeedPoller: |
There was a problem hiding this comment.
CLAUDE.md docs convention violation — FeedPoller 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
lifespanstarts 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 byFEED_POLLER_ENABLED.
Summary
Closes #37. Builds on the cache layer from #36 to keep the feed live without manual reload.
Backend
FeedPollerasyncio task wired into the FastAPI lifespan alongsidePRWatcher. Walks every credential row every 120s, calls the matching feed fetcher, overwrites the cache. Env-gated viaFEED_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 sourceFEED_POLLER_ENABLED=trueadded to.env.example; tests set itfalseinconftest.pysoTestClient(app)doesn't spin up a real loopFrontend
useEffectinAgents.tsxfiressetInterval(120_000)to re-pull from the (warm) cache. NofeedLoadingflicker, no filter wipeuseEffect(() => 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 passbun run lint && bun run build && bun run test— frontend clean/agents, wait ~120s, watch DevTools Network for the three/gateway/*re-fetches. UI updates silently (no spinner)pr), wait through a tick, confirm the filter is still appliedfeed_poller: refreshed N cache entriesevery 120sCtrl-Cthe backend — clean shutdown, both workers log "shutting down"🤖 Generated with Claude Code