All notable changes to StarMapper, ordered by release.
cacheComponents: true enabled in next.config.ts. All data-fetching pages migrated from export const revalidate = N (time-based) to 'use cache' + cacheTag + cacheLife (tag-based, on-demand). Cache invalidation is now surgical: POST /api/badge-update invalidates only the affected repo, the cron MV refresh invalidates trending and explore-mvs, POST /api/news invalidates the author's feed. Three shared query libs extracted (repos-query.ts, trending-query.ts, devs-query.ts) so pages and API routes share the same cached functions./, /repos, /trending, /devs/atlas, /devs) were making HTTP fetch requests to their own API routes (e.g., fetch("http://localhost:3000/api/trending/repos")). All five now call the DB lib directly, eliminating the unnecessary loopback latency./repos, /trending, /devs, /devs/atlas migrated from useEffect waterfall fetch to async server components with initialData prop. Data is available on first paint with no client-side loading state./about, /privacy, /terms, /oss, etc.) marked force-static so they are prerendered at build time and served from the CDN edge with no runtime cost.StatsModal, ShareModal, AllStargazersModal, GrowthModal, BadgeModal, RateLimitedModal, RepoNotFoundModal are now dynamic() imports. Reduces initial JS bundle parsed on page load.t = zA/(zA-zB)) and used as the clip point. Continents now fade out gradually at the globe edge. Fast paths retained for rings fully in front or fully behind.src/lib/resolve-base-url.ts. Removed in subsequent refactor once the self-call pattern was eliminated.DB_STORAGE_LIMIT_MB env var removed; hardcoded to 100 GB to match Neon sponsored plan. Removes an unnecessary configuration surface.repos route mock aligned to $queryRaw (was badgeCache.findMany). Ratelimit stub fixed for TS2556 spread unknown[].src/env.ts added via @t3-oss/env-nextjs. Build fails at compile time and server startup if DATABASE_URL, GITHUB_TOKEN, or NEXT_PUBLIC_JAWGMAP_ACCESS_TOKEN are missing. Prevents silent misconfiguration on new deployments (closes #5).GET /api/trending/repos and GET /api/trending/map replace the monolithic GET /api/trending. The repos list now renders before the map because the two fetches are independent. Map endpoint decompresses the top 5 repos (was 10), halving CPU work per request. /trending loading skeleton added via loading.tsx. Legacy route kept as alias for one cycle.style-src 'unsafe-inline' replaced with CSP Level 3 split: style-src-elem 'self' 'nonce-{nonce}' (blocks <style> injection) and style-src-attr 'unsafe-inline' (scoped to element attributes only, required by React dynamic styles and MapLibre controls). Closes #56.ci.yml, audit.yml, semgrep.yml) now reference actions by full commit hash instead of mutable version tags. Eliminates supply-chain risk from tag mutation. Closes #55.link-check.yml workflow runs lychee monthly against starmapper.bruniaux.com, catching dead links and 404 regressions automatically.src/app/[owner]/[repo]/page.tsx refactored across 12 commits. Extracted components: StatsModal, ShareModal, AllStargazersModal, GrowthModal, BadgeModal, RateLimitOverlay, PreScanOverlay, RateLimitedModal, RepoNotFoundModal, GrowthChart. Extracted hooks: useScanController, useRepoCacheLoader, useCompareScan, useWatchMode, useTimelapse. Each extraction ships with its own unit tests.#faf6ed background, orange accent) across all light-mode CSS tokens in globals.css. Hero globe adapts to the active theme.StargazerMap, CountryChoropleth, and LanguageChoropleth now render a fallback message instead of crashing when WebGL is unavailable (headless environments, some enterprise proxies).AbortController added to 7 async fetch effects in the map page (repo-info, stats, organic-score, compare-info, growth-data, geo-velocity, stargazer-cache check). Prevents state updates on an unmounted component when the user navigates mid-scan. useCompareScan threads AbortSignal into each /api/chunk fetch. Closes #47./profile/[login]) columns stack vertically on mobile (flex-col lg:flex-row). Action buttons (GitHub, Refresh, LinkedIn, Contact) are icon-only on mobile. Contact dropdown becomes a bottom-sheet. New reusable src/components/ui/tabs.tsx component.snap-x snap-mandatory).opengraph-image.tsx are now caught and rendered as a text fallback instead of silently failing. Closes #49.POST /api/vitals logs structured JSON instead of a raw string; React list-key instability in Explore fixed. Closes #57, #58.package.json now runs prisma generate on every pnpm install. Fixes the case where pnpm reorganizes the virtual store and wipes the generated .prisma/client artifacts.ThemeToggle and TokenModal. 19 new tests for trending endpoints. 16 smoke tests for extracted map components. 8 tests for useRepoCacheLoader, 4 for useCompareScan. Total: 856 → 872 tests.LocalCache helpers (loadCache, saveCache, clearCache, cacheKey) centralized from inline page.tsx definitions into a shared lib. Two sequential useEffect that both called loadCache merged into one, removing a duplicate localStorage read on every page load.prisma migrate diff --from-empty. Combined with prisma/sql/views.sql, gives contributors a complete DB picture without enabling migration history. Closes #53.MASTER.md, 209 lines) removed; globals.css documented as the single source of truth for tokens. Closes #54.react-hooks/exhaustive-deps flipped from off to warn. 5 pre-existing intentional violations suppressed with inline comments explaining the rationale. Closes #48.@upstash/redis, eslint-plugin-react-hooks, @types/node, tsx bumped..claude/rules/ files ported/adapted from methode-aristote: response-discipline, git-merge-discipline, react-performance-optimization, react-timers-cleanup, typescript-zero-errors, session-management, scripts-best-practices, file-organization, universal-rules, known-gotchas. CLAUDE.md slimmed from ~800 to 146 lines./faq dedicated page replaces the inline FAQ section; a compact teaser remains on the landing with a "See all" link.fetchAndPatchStyle auto-switches to NEXT_PUBLIC_JAWGMAP_ACCESS_TOKEN_2 when the primary token returns 401/402/403/429 (Map Views limit). Transparent to users, no reload required.src/lib/scan-cache.ts added (preparatory, not yet wired into the chunk loop). Persists scan results between page reloads for returning visitors./trending added to sitemap and navigation./vs/star-history with structured data and UTM tracking on outbound badge/embed links.description, og:*, twitter:* fixed on all pages.$executeRawUnsafe replaced with $executeRaw + Prisma.sql on all MV refresh paths. Admin endpoints return 404 (not 401/403) on auth failure to avoid endpoint discovery.useEffect + useState fetch with useSWR; stale-while-revalidate reduces perceived latency.stargazer_cache SELECT restricted to points only (was fetching the full row including unmapped).ILIKE search on login replaced with an IN clause to avoid a full table scan.max-w-7xl across all pages.FlorianBruniaux (camelCase) used consistently in profile and badge URLs.github_user has multiple casing variants for the same login, the record with the most data is selected instead of throwing.pnpm/action-setup reads packageManager from package.json instead of hardcoded version. Semgrep false positives on JSON-LD suppressed.graphify-out/ untracked (328 files, 5.8MB of generated cache removed from git history going forward). .gitignore extended with .pnpm-store/, .codex/, .code-review-graph/.star_event.starredAt via GET /api/stats/[owner]/[repo]/growth (SQL DATE_TRUNC('week'), 5-min CDN cache). Falls back to in-memory starredAt timestamps for repos scanned in the current session. Button is now visible for any repo with scan data, not just scans that captured timestamps in memory./api/repos?diverse=true mode: fetches a 500-row pool and filters to max 3 repos per owner + min 100 stars before returning results. Prevents a single active user from filling the entire grid.github.com/[login]), a "★ StarMapper" button is injected in the user sidebar that opens starmapper.bruniaux.com/profile/[login]. Content script matches extended to ["https://github.com/*", "https://github.com/*/*"] to cover single-segment paths. getPageContext() discriminated union (repo | profile | other) dispatches the correct button per page type. Profile button is full-width to match GitHub's sidebar style; injection targets .js-profile-editable-area then Layout-sidebar with a 2s floating fallback./owner/repo page, opening the StarMapper map directly. Toolbar popup with the current repo + last 5 visited repos + search by slug or URL. Context menu on right-click for GitHub links. Handles GitHub SPA navigation (Turbo + bfcache) via MutationObserver. Dark/light compatible via GitHub CSS variables.@crxjs/vite-plugin v2 (stagnant beta) with WXT v0.20. entrypoints/ structure (background.ts, content.ts, popup/), build → .output/chrome-mv3/, wxt zip for Chrome Web Store. Standalone tsconfig.json (without extends: .wxt/tsconfig.json) to avoid the Vite circular reference bug.MutationObserver replaces setTimeout(120ms) for button injection; pageshow handler + e.persisted for bfcache; recent repos saved to chrome.storage.local on click; SYSTEM_OWNERS blocklist in context menu (filters /settings, /explore, etc.); icons in public/icons/ for WXT serving.README.md (Chrome Extension), PITCH.md + PITCH-en.md (Organic Score: 4 signals → 3, correct weights fork=40%/watcher=5%/zero-followers=55%; Watch mode, Geo velocity, Notable stargazers, Chrome Extension added), docs/ARCHITECTURE.md (version 0.4.6, Neon 100GB sponsored, +15 missing API routes, full file structure with schemas/ and extension/), PROJECT_INDEX.md + llms.txt (extension/ section), docs/organic-score-calibration.md (Final Decision corrected: fork=40%, non-fork=70%).extension/ excluded from root compilation (WXT config is standalone in extension/tsconfig.json).+N ★ · India, Germany in real time. Stops automatically after 10 min with no new star. Endpoint GET /api/watch/[owner]/[repo]?since=<ISO>: GitHub REST + countryNormalized lookup from github_user (no Nominatim calls). No DB writes, Cache-Control: no-store.rising (×1.5+), new (no history), stable, declining (≤0.5). Lazy loading: the request only fires when the tab is opened, once per session. Endpoint GET /api/stats/[owner]/[repo]/geo-velocity, SQL query with COUNT(*) FILTER, 5-min CDN cache.+N/mo in green under the star count, computed from starredAt already in memory after a scan. Only appears when the data is present (recent scans with timestamps); silently absent for old caches.defineRoute(schema, handler) wrapper. New src/schemas/ directory holds typed Zod v4 schemas for each route (track, vitals, recalculate-location, badge-update, chunk, news, stargazer-cache). Per-field error codes are declared directly in schemas; defineRoute surfaces issues[0].message verbatim so every existing error contract is preserved unchanged. Manual typeof / regex validation chains removed from all route handlers. getIP exported from api-helpers to replace a duplicate helper in the chunk route./profile/[login] now shows the cached top repos grid (up to 8, from topRepos in DB). Count badge reflects the real publicRepos value from GitHub, not the cached repo list length.@ (e.g. @ruvnet) now works the same as without. The prefix is stripped before debouncing to the search state.topReposFetchedAt is reset so the next profile load re-fetches top repos from GitHub instead of serving the outdated cache.CHANGELOG.md at build time. Server Component with inline bold+code rendering without an external markdown dependency. Link added in the footer and in the announcement banner.user_repo_count_mv over 12k rows via Neon was exceeding the 10s statement timeout. Fixed by pushing the lat IS NOT NULL filter before the JOIN and capping candidates to 500 before enrichment.w-96 with more padding to prevent RSS/JSON URL wrapping./feed/[login], the dropdown was redundant (URLs already displayed). minimal prop added: direct toggle without dropdown.NewsTimeline component integrated on /profile/[login] with skeleton loader, conditional "Publish" button (visible only if the stored token matches the page login).GET /api/feed/[login]/rss (RSS 2.0 with <atom:link>, If-Modified-Since, 304 response) and GET /api/feed/[login]/json (JSON Feed 1.1). Cached 1h CDN. Subscribe link (RSS icon + "Subscribe") displayed at the top of the News section on the profile./api/feed/[login]/rss is recorded in page_view (type "feed_rss", slug = login). Queryable via pnpm stats:views.unsafe-inline in CSP./profile/[login]), getStoredUsername() returned "", which set isOwner to false and hid the "Publish" button even for the profile owner. handleSave now resolves the login via GET /api.github.com/user and stores it. "Verifying…" shown during verification; handleRemove also clears the username.buildRss20() (RSS 2.0 XML, correct CDATA, ]]> split into two CDATA sections) and buildJsonFeed() (JSON Feed 1.1 object). Build logic decoupled from routes for testability.github-auth.ts, reused by all news and feed routes.BANNER_ID; bump the ID to make it reappear for the next announcement. Home header switched to sticky to stack naturally below the banner.PostToolUse hook that detects the creation of a new page.tsx or route.ts and reminds you to update AnnouncementBanner.opacity-0) for users *with* coordinates, and visible (grey) for those *without*. Inverted: button always visible and clickable for geolocated users, invisible (space preserved) for others.country_stats_mv was created empty (no countryNormalized at creation time), then never refreshed after the backfill. Added create:country-stats-mv / create:country-stats-mv:prod commands to create and refresh the MV.repo-metrics + repo-languages). Commands: update:prod, update:local, update:local:force.DATABASE_DRIVER=standard.OrganicScorePill fetched independently from the rest of the page.openIssuesCount field). Organic score modal displays both badges separately and as clickable links (GitHub links).stats/[owner]/[repo] was throwing a Neon timeout on large tables. Graceful fallback: returns the partial data available without crashing.backfill-repo-metrics.ts was using DATABASE_URL_LOCAL instead of DATABASE_URL for :prod commands. Fixed + NEXT_PUBLIC_ORGANIC_SCORE_ENABLED=true forced.docs/organic-score.md: StarScout vs StarMapper comparison, normalization formula, known limitations./profile/[login]: two-column layout (scrollable panel 2/3 + sticky map 1/3). Data: bio, followers, repos, languages, tracked star events, contribution by country. Partial profile if user is absent from DB (refresh triggered automatically).explore/top, explore/power, explore/nearby. "View StarMapper profile →" button in the stargazer popup.POST /api/track triggered on load; daily view counter per profile in page_view.GET /api/geo/[owner]/[repo]: aggregated endpoint returning GeoJSON points from a scan, protected by HMAC API key. Usable by third-party tools.star_event.starredAt.startTransition around chunk loop dispatches, useDeferredValue on the stargazers filter, useCallback on map handlers, gating of expensive memos, lazy-load TokenModal + SponsorsBlock, width/height on avatars (CLS).stargazer-cache, optimized CDN TTL, redundant login index removed.setData window to avoid blocked frames..gitignore.db:sync:from-neon: --repo, --limit, --tables variants for partial sync. SET statement_timeout=0 added at the top of all DDL scripts (indexes + MVs). Prisma slow query logger enabled.user_repo_count_mv (per-user repo count, nearby query 6s→200ms). GIN trigram index on login + name (ILIKE search 6s→50ms).callJawg() in geocoder.ts was sending the token only via the x-api-key header. Added the access-token query param required by the dedicated starmapper.jawg.io endpoint. Without this param, Jawg requests silently returned 401./api/explore/geocode was manually reconstructing the label using p.city (a field that does not exist in the Jawg Places model). Replaced with feature.properties.label, which Jawg provides natively.geocoder.test.ts was stubbing JAWGMAP_ACCESS_TOKEN while the code reads JAWG_TOKEN_HEADER. 7 pre-existing tests were silently failing. Fixed.README.md and docs/ARCHITECTURE.md. Jawg Places is based on Pelias but the correct brand name is Jawg Places.stargazer-map.tsx (30 lines, no cache) and a version in lib/map-style.ts (with cache). lib/map-style.ts is now the single source of truth. The function accepts a projection parameter ("mercator" | "globe", default "mercator") with a composite cache key ${url}#${projection} to avoid globe/mercator collisions.lang=en removed from style URLs in theme.ts and stargazer-map.tsx (Jawg handles language natively). Glyphs URL patch removed (lib/map-style.ts, stargazer-map.tsx). name:fr → name:en replacement removed (map-style.ts, stargazer-map.tsx).api.jawg.io to starmapper.jawg.io (dedicated StarMapper endpoint). Token migrated from JAWGMAP_ACCESS_TOKEN to JAWG_TOKEN_HEADER. Added x-api-key header.scripts/ now use node:util parseArgs with strict: true instead of ad-hoc process.argv.includes / getArg patterns. parseArgs is native Node 18+, no dependency./devs/atlas page: world choropleth map showing the most popular language per country, computed from starred and contributed repos. Country detail on click (dominant language, %, number of devs). "Early preview" banner while the backfill runs./devs and /devs/[language] pages: developer map filtered by language, with a selection combobox.backfill-languages.ts script to populate the languages[] field on github_user. --from-cache mode: derives languages from star_event + badge_cache with no GitHub API call (1.23M users in seconds). API mode: parallelizable via --token-index.pnpm db:sync and the daily admin cron.maxRepositories: 30 → 10 (fewer GraphQL points consumed), default batch 10 → 50, bulk UPDATE via unnest() (1 SQL query instead of N individual ones).github_user was going from DO NOTHING to DO UPDATE: the languages and languagesFetchedAt columns were never being pushed to Neon. Fixed.country_language_stats_mv on Neon during sync if it does not yet exist.src/lib/language-colors.ts.ssr: false).innerHTML to DOM API construction.unsafe-eval removed in production. HSTS added.max-w-7xl.POST /api/stargazer-cache. Fixes silent cache loss on repos with >~15k stars (raw payload ~15MB > Vercel 4.5MB limit). Payload reduced to ~800KB.[object Object], XSS artifacts, Jinja templates).db.ts: DATABASE_DRIVER=standard → @prisma/adapter-pg (Docker, Railway, Supabase); default → @prisma/adapter-neon. Self-hosting without Neon works.take: 10_000), TTL-aware health guard.worker-src blob: (was blocking the MapLibre web worker → blank map on some configs).localStorage access during SSR render caused React error #418. Fix: state initialized SSR-safe, synced via useEffect.cacheCheckDone state.isGeocodeableLocation filter extended to prefixes #$<>[{"!.api-validation.ts, api-helpers.ts, compression.ts, compress-client.ts. Replaced 10 duplicated patterns across 15 route handlers.interface → type. import type for type-only imports.batch-scan.ts: incremental FLUSH_EVERY writes, session-level geocoding cache, better error recovery./api/badge/[owner]/[repo] — shield with mapped count and country count, 6h CDN cache.robots.txt, sitemap.xml, structured FAQ, Open Graph metadata./explore listing mapped repos with stats.stargazer_cache table) — instant reload for subsequent visitors on the same repo. Limit: 100k stars.lat=null/lng=null — avoids repeated API calls for the same garbage input.isGeocodeableLocation() filters TLDs, phone prefixes, URLs, placeholder values before any API call.POST /api/chunk calls (100 users/call) to stay under the Vercel 10s timeout.geocache Neon table shared across all repos — a location geocoded once benefits all future scans.github_user + star_event tables to track users and their repos at the user level.Follows Semantic Versioning — format inspired by Keep a Changelog.