feat(stats): server-side GitHub stats cache + client swap (stop burning visitor rate limit)#61
Merged
Conversation
- Add github_cache SQLite table (1 row per endpoint_id, idempotent IF NOT EXISTS) - Add 4 prepared statements + 4 public wrappers in queries.js - New src/telemetry/github-poller.js mirroring housekeeper.js (5min interval, setImmediate first tick, busy guard, ETag round-trips, never throws, reads GITHUB_TOKEN once at boot, single boot warning when absent) - Extend public-stats router with GET /github/:endpoint_id (memo + ETag + Set-Cookie strip; 400 on allowlist miss, 503 + Retry-After on cache cold, 304 on If-None-Match match, 200 with cached JSON otherwise) - Wire startGithubPoller(db) next to startHousekeeper(db); clear interval in SIGINT + SIGTERM handlers - Tighten CSP connect-src back to 'self' (api.github.com removed) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndpoint - Replace API_BASE='https://api.github.com' + per-endpoint URLs with API_ROOT='/api/public-stats/github' single-line constant - Delete fetchPaginated (server returns concatenated arrays in one response) - Delete 403 + X-RateLimit rate-limit branch (dead code post-swap) - Delete emitRateLimited helper (no rate-limited subject emissions possible) - Drop X-GitHub-Api-Version + application/vnd.github vendor Accept headers; server handles all GitHub-side specifics - Add 503 -> null path so first-boot cache cold is treated as transient - Update file-header design notes block accordingly - Aggregator functions UNCHANGED (cumulativeStarsSeries etc.) -- raw GitHub JSON shape preserved verbatim by the server - Switch fetch to credentials: 'same-origin' (was 'omit') - Delete tests/showcase-csp-allows-github-api.test.js (obsolete regression guard for the now-reverted CSP api.github.com allowance) - Remove the deleted test from package.json scripts.test chain Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests - tests/server-github-cache.test.js: 17 assertions across T1-T6 - T1: seeded GET /github/repo-summary -> 200 + ETag + Cache-Control + body - T2: If-None-Match round-trip -> 304 empty body, ETag preserved - T3: cache-cold GET -> 503 + Retry-After: 60 + JSON error shape - T4: unknown endpoint_id -> 400 allowlist rejection - T5: stars endpoint serves array; no Set-Cookie defensive check - T6: _resetMemoForTest forces DB rebuild; new ETag differs - tests/server-github-poller.test.js: 29 assertions across T1-T7 - T1: full happy-path tick caches all 7 endpoint_ids with status=200 - T2: 304 path preserves payload_json, bumps fetched_at, sets status=304 - T3: one endpoint rejection -> tick never throws; others still upsert - T4: GITHUB_TOKEN set -> Authorization: Bearer header sent - T5: GITHUB_TOKEN unset -> no Authorization header - T6: commits pagination concatenates pages 1+2+3 = 250 elements - T7: GITHUB_ENDPOINT_IDS allowlist invariant (length 7, exact membership) - Uses require.cache deletion to re-evaluate GITHUB_TOKEN per scenario - Wire both new tests into package.json scripts.test chain immediately after server-public-stats-no-auth.test.js (before fsb-telemetry-service) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests After 260516-7l5 the Angular client no longer paginates against GitHub; the server-side poller does the walk. Drop the now-defunct `MAX_PAGES = 30` assertions from tests/cumulative-commits-aggregator.test.js and move the equivalent regression guard to tests/server-github-poller.test.js (new T8: source-grep MAX_PAGES_COMMITS >= 30 in github-poller.js). The cumulative-commits aggregator function itself is unchanged and still guarded by tests 2-12 in that file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
LakshmanTurlapati
added a commit
that referenced
this pull request
May 18, 2026
* docs: start milestone v0.9.70 Showcase Dashboard Reliability (Streaming + Sync + Viewport) Three target features: (1) diagnose-first dashboard DOM-streaming fix continuing STREAM-07 attempt 2 of 5 from v0.9.69 Phase 276; (2) Sync-tab remote-control restoration on full-selfbrowsing.com; (3) 16:10 desktop viewport lock on the dashboard preview pane (mobile out of scope). PROJECT.md "Current Milestone" rewritten to v0.9.70 with goals + target features + key constraints + deferred candidates. Footer "Last updated" bumped to 2026-05-16. "Last shipped" updated to reflect v0.9.69 SHIP + four post-milestone fixes (PR #57/59/61/62/63/64) and the published artifacts (extension v0.9.67 zip, mcp-v0.9.2 npm, Fly auto-deploy). STATE.md frontmatter reset (milestone v0.9.70, status in_progress, progress 0%). Current Position set to "Not started (defining requirements)". Active Milestone Risk Register replaced with Active Milestone Carry-Forward (STREAM-07 attempt 2 + CWS click-through). Quick Tasks Completed table extended with PRs #59 / #61 / #62 / #63 / #64. Pending User-Gated Actions: v0.9.0 npm publish marked superseded by v0.9.2; added v0.9.67 -> extension-v0.9.67 retag decision. Phases dir cleared (8 v0.9.69 phase directories removed by gsd-tools phases clear). Carries over: 999.1-mcp-tool-gaps, INTEGRATION-CHECK.md, v0.9.63-INTEGRATION-CHECK.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(showcase): lock dashboard preview pane to 16:10 on desktop Phase 280 VIEWPORT requirement: the live DOM preview frame in the showcase dashboard had no fixed aspect ratio and stretched freely to container height, producing inconsistent letterboxing and the bug visible in milestone 0.9.70. Pin it to 16/10 on desktop, unset on mobile (<=768px and <=480px) where vertical layout takes over, and keep Maximized (100vh) and PiP layouts coherent (auto for maximized, explicit 16/10 for PiP). Adds tests/dashboard-preview-aspect-ratio.test.js — parses the SCSS and asserts the rule lives in the right selectors (desktop, both mobile media queries, Maximized, PiP). Wired into the root `npm test` chain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Move all GitHub API calls off the Angular client onto a server-side 5-min poller backed by SQLite. The /stats page now reads from same-origin
/api/public-stats/github/:endpoint_idinstead ofapi.github.comdirectly. Stops every visitor's browser from burning the shared 60-req/hr unauthenticated GitHub rate limit and lets one authenticated server poller use the 5000-req/hr quota.Why now
Quick task
260514-r6ihad to widen the showcase CSP to allowapi.github.combecause /stats was hitting GitHub from the visitor's browser. Under any non-trivial traffic the shared per-IP rate limit gets exhausted and charts go dark. The right pattern is: server polls once, persists, serves from cache; this PR implements that.Design
Cache table (
showcase/server/src/db/schema.js)github_cachekeyed byendpoint_idPRIMARY KEY, withpayload_json/etag/fetched_at/http_status/rate_limit_remaining/rate_limit_reset. Added inside the existing idempotentCREATE TABLE IF NOT EXISTSblock so deploy auto-creates it on the Fly volume.Poller (
showcase/server/src/telemetry/github-poller.js)Mirrors the existing
housekeeper.jscron pattern:setImmediatefirst tick +setIntervalevery 5 minutes, defensive try/catch wrapping each endpoint and the outer tick body (never throws),pollerBusyoverlap guard. ReadsGITHUB_TOKENonce at module load — works with (5000/hr authenticated) and without (60/hr unauthenticated, single boot warning, no per-tick noise). 7 endpoints:repo-summary,stars,issues,forks,pulls,commits,releases.If-None-MatchETag round-trip turns steady-state cycles into mostly-304s that don't count against the rate budget.Endpoint (
showcase/server/src/routes/public-stats.js)New sub-route
GET /api/public-stats/github/:endpoint_idmirroring the existing/globalpatterns: in-process Map memo, sha256 ETag,Cache-Control: public, max-age=60, defensiveres.removeHeader('Set-Cookie'), allowlist guard returning 400 on unknownendpoint_id, 503 +Retry-After: 60on cold cache, 304 onIf-None-Matchmatch.Angular swap (
showcase/angular/src/app/core/stats/github-stats.service.ts)API_BASE,MAX_PAGES,PER_PAGE,fetchPaginated,emitRateLimited, the 403 rate-limit branch, and theX-GitHub-Api-Versionheader are all gone.API_ROOT = '/api/public-stats/github'withcredentials: 'same-origin'. Aggregator functions and component consumers are unchanged.CSP cleanup (
showcase/server/server.js:63)connect-srctightened from'self' https://api.github.comto'self'. The now-obsoletetests/showcase-csp-allows-github-api.test.jsis deleted and removed from the npm test chain.Tests
tests/server-github-cache.test.js(NEW, 17/17 PASS): 200+ETag round-trip, 304 path, 503+Retry-After on cold cache, 400 on unknown id, no Set-Cookie leak, memo rebuild.tests/server-github-poller.test.js(NEW, 31/31 PASS): tick upserts, 304 preserves payload, error resilience, GITHUB_TOKEN header on/off, pagination concat, allowlist invariant, T8 source-grep guard thatMAX_PAGES_COMMITS >= 30(regression migrated from the now-irrelevant client-sideMAX_PAGES = 30assertion incumulative-commits-aggregator.test.js).tests/cumulative-commits-aggregator.test.js: trimmed by 2 assertions (MAX_PAGES moved to poller test); aggregator function coverage unchanged.Validation gates
node scripts/validate-extension.mjs— OK (manifest valid, 245 JS files parsed clean)npm --prefix showcase/angular run build— exit 0npm test— exit 0 (full root suite)Verifier verdict
human_needed— 13/15 code-level truths verified by static analysis + unit tests; 2 truths route to post-deploy DevTools confirmation:api.github.comrequests on /stats (provable indirectly via CSP + service source, final confirmation needs Network panel inspection).Operational follow-up (user-gated)
Set the GitHub token on Fly so the poller uses the 5000/hr authenticated quota instead of the 60/hr unauthenticated fallback:
The poller works without it (single boot warning, falls to unauthenticated 60/hr — still vastly better than every visitor sharing that pool). Do NOT put the token in
fly.toml.Test plan
GITHUB_TOKENviafly secrets./api/public-stats/github/repo-summaryreturns 200 after the first 5-min tick.api.github.com— confirm ZERO requests, all GitHub data sourced from same-origin.🤖 Generated with Claude Code