This file gives AI coding agents (Claude, Copilot, Cursor, etc.) the context needed to work on OrgExplorer without re-deriving architecture from scratch. Read this before making changes.
OrgExplorer is a dashboard for exploring GitHub organizations — repositories, contributors, governance health, network graphs, and activity trends. It's a React + Vite frontend that talks directly to the GitHub REST API from the browser (no backend server).
This project originates from AOSSIE's GSoC idea proposal (see [https://github.com/AOSSIE-Org/OrgExplorer]), which set three hard requirements:
- No backend — must run entirely in the browser.
- Local storage / IndexedDB only for persistence.
- Aggressive local caching to avoid exceeding GitHub API limits.
Everything else in this codebase is downstream of those three constraints. OrgExplorer prioritizes:
- GitHub API efficiency over completeness when unauthenticated. Standard mode exists because the app has no server to proxy requests through or hide a shared token behind — every unauthenticated visitor shares GitHub's 60 req/hr limit individually. Scoping to top-10-per-org is a deliberate trade-off, not a shortcut to remove later.
- Browser-only architecture (no backend). Do not introduce a server, serverless function, or proxy to work around rate limits or CORS — solve it client-side (PAT support, caching, batching) or don't solve it.
- Derived analytics instead of raw GitHub listings. Prefer computing a health score, ranking, or trend over just displaying a raw API response. The value of this app is the synthesis, not the listing.
- Deterministic and explainable metrics. Every score (health score, repo ranking, bus factor, etc.) should be traceable to a documented formula — no black-box weighting, no randomness, no metric a user can't get an explanation for via a "Learn More"-style modal.
- Fast repeat visits through aggressive caching. IndexedDB caching
(
services/cache.js, 1hr TTL) exists specifically so a user re-opening the same org doesn't re-spend their rate limit. Any new fetch path should go throughfetchWithCache, not a rawfetch().
When multiple implementations are possible, prefer the one that minimizes API requests while keeping analytics deterministic.
React, Vite, TailwindCSS, Recharts, D3, React Router.
Prefer these existing libraries before introducing a new dependency. If you believe a new library is genuinely needed, say so explicitly in the PR description rather than adding it silently.
Note: services/cache.js uses the native browser indexedDB API directly
(not the idb npm wrapper). Do not add idb as a dependency to "simplify"
this — it would mean rewriting working, tested code for no functional gain.
This shapes almost every architectural decision in the codebase. Unauthenticated requests are capped at 60/hr; requests with a Personal Access Token (PAT) get 5,000/hr. Because of this, the app runs in one of two modes:
- Standard mode (no PAT) — analyzes a scoped-down subset of the org to stay within the unauthenticated limit.
- Complete mode (PAT connected) — analyzes the full org.
Every data-fetching function needs to respect this distinction. Do not add a
fetch that ignores the pat presence check.
// services/analytics.js
export function getTopRepositories(repos, limit = 10) {
// Score = stars + 2×forks + 1.5×watchers + recency bonus
// (bonus decays linearly for repos not pushed to within the last year)
}// context/AppContext.jsx
const selectAnalysisRepos = useCallback((allRepos) => {
const byOrg = {}
for (const repo of allRepos) {
(byOrg[repo.orgLogin] ??= []).push(repo)
}
return Object.values(byOrg).flatMap(orgRepos =>
pat ? orgRepos : getTopRepositories(orgRepos, 10)
)
}, [pat])Rule: without a PAT, only the top 10 repos per org (by this scoring formula) are used for contributors, issues, and pull requests. With a PAT, all fetched repos per org are used.
If you add a new feature that fetches per-repo data (e.g. a new metric type),
it must go through selectAnalysisRepos (or an equivalent that applies the
same rule) — not model.totalRepos directly, and not a different top-N cutoff.
Inconsistent scoping across features is a recurring bug class in this repo;
see git history around "PR-metrics scoping" for a real example.
- Health Score
- Activity Classification (repo-level; previously called "lifecycle" in
some older code/tooltips — that naming is deprecated, use
activityClassification) - Bus Factor
- Contributor Freshness
- Issue Resolution Rate
- Cross-repo contributor signal
These metrics are deterministic — same inputs always produce the same output, no randomness, no hidden weighting.
Never modify a metric's formula without updating both its documentation here and every page that consumes it.
Note: if additional metrics exist in the codebase beyond this list, add them here with their formula reference — don't let an agent infer meaning from variable names alone.
Unless explicitly requested, do not change:
fetchWithCache()buildAnalyticalModel()AppContextstate shape- PAT save/load flow
- Cache TTL (1 hour)
- Health score formula
getTopRepositories()/selectAnalysisRepos()scoring and scoping logic
These are load-bearing for every page in the app. A change here has a wide blast radius — treat it as an architecture change, not a local fix.
| Data | No PAT | With PAT |
|---|---|---|
Repos fetched (fetchRepos) |
≤500/org (5 pages) | all repos/org (Math.ceil(public_repos/100) pages) |
| Repos used downstream | top 10/org | all fetched repos/org |
| Contributors/repo | ≤100 (1 page) | ≤1000 (10 pages) |
| Issues/repo | ≤100 (1 page) | ≤1000 (10 pages) |
| Pull Requests/repo | ≤100 (1 page) | ≤1000 (10 pages) |
These caps live in services/github.js inside each fetchX function as
maxPages = pat ? 10 : 1 (or 5 for the initial repo list). Keep them
symmetric across fetchContributors, fetchIssues, and fetchPulls unless
there's a specific reason to diverge — they were intentionally aligned.
Single context provider (AppProvider) holds all cross-page state. Key pieces:
model— built bybuildAnalyticalModel(), containsallRepos(PAT-scoped, used for contributors) andtotalRepos(raw, unscoped fetch — use this +selectAnalysisReposfor new per-repo fetches, notallRepos).isComplete— true once org metadata + all repos + all contributors are fetched (i.e.explore()ran with a PAT). Overview, Contributors, Repositories, and Network pages gate their "Run Complete Analysis" banner on this flag.auditComplete— true once issues are fetched for the full repo set. Used by the issue/governance side of Analytics and Governance pages.advanceAnalyticsComplete— same idea, for pull-request data.lastOrgNames— remembers the last searched org(s) so any page can re-triggerexplore()without the user re-entering names.
| Function | Fetches | Use when |
|---|---|---|
explore(orgNames) |
org metadata, repos, contributors | Initial search |
runFullExplore() |
same as explore, replays lastOrgNames |
Any page's banner needs to (re-)run a full analysis |
runAudit() |
issues only, from current model |
Rare — most callers should use runGovernanceAnalysis instead |
runGovernanceAnalysis() |
repos (if needed) + issues | Governance page trigger |
runAdvanceAnalytics() |
repos (if needed) + pulls | Analytics page (PR metrics) trigger, no-PAT case |
runFullAnalytics() |
repos (if needed) + issues + pulls (parallel) | Analytics page combined trigger, used once PAT is connected |
Pattern to follow for any new "Run Complete Analysis" feature:
const runSomeNewAnalysis = useCallback(async () => {
if (someLoadingFlag) return
let currentModel = model
if (!isComplete) {
setSomeLoadingFlag(true)
const freshModel = await runFullExplore()
setSomeLoadingFlag(false)
if (!freshModel) return
currentModel = freshModel
}
if (!currentModel) return
setSomeLoadingFlag(true)
const repos = selectAnalysisRepos(currentModel.totalRepos)
// ...fetch and process...
setSomeLoadingFlag(false)
setSomeCompleteFlag(!!pat)
}, [isComplete, model, runFullExplore, selectAnalysisRepos, pat, someLoadingFlag])This avoids a real stale-closure bug that's bitten this codebase before: don't
read model from context state right after calling runFullExplore() — state
updates aren't synchronous, so use the model returned by the explore call
instead.
Shared banner shown at the top of Overview, Repositories, Contributors, Network, Governance, and Analytics. It:
- Shows "Standard" vs "Complete" status via an
analysisStatusprop ('standard' | 'complete'). - On click, checks if a PAT is saved; if not, opens
PATModalinstead of running anything. - Calls the page's
onRuncallback (one of therunXfunctions above) once a PAT exists.
When adding this banner to a new page, wire analysisStatus to whichever
*Complete flag actually reflects that page's data — don't reuse
isComplete for pages whose data depends on a later fetch stage (issues,
pulls) that could still be standard-scoped even after isComplete is true.
- Reuse existing shared components (
AnalysisBanner,PATModal, card/stat components incomponents/UI) rather than building page-specific one-offs. - Reuse existing skeleton loaders (e.g.
OverviewSkeleton) rather than introducing a new loading pattern per page. - Every page should account for all five states: loading, empty, error, standard mode, and complete mode. Don't ship a page that only handles the happy path.
- Loading checks must run before any early
return nullfor missing data — see "Common bugs" below on hook ordering.
Maintain across all UI work:
- Keyboard navigation for interactive elements (modals, dropdowns, buttons)
- Semantic HTML over div-soup where practical
- Visible focus states
aria-labels on icon-only buttons- Sufficient color contrast against the existing dark theme
Note: as of the current codebase, PATModal is missing dialog semantics
(role="dialog", aria-modal, focus trap, Escape-to-close) — this is a
known gap, not a pattern to replicate in new modals.
Handling of the GitHub PAT:
- Never log the PAT, in console output, error messages, or analytics.
- Never send the PAT anywhere except directly to
api.github.com. - Never persist the PAT outside
localStorage(no cookies, no query params, no external storage). - Never expose the PAT in a URL, including in
Linkheaders or debug output.
Note: the PAT is currently stored as plaintext in localStorage
(oe_pat), which is readable by any script able to execute in this origin.
This is a known, accepted trade-off for a backend-less app — do not "fix"
it by adding a server or remote storage without discussing it first, since
that would violate the no-backend requirement.
services/cache.js wraps every GitHub request in an IndexedDB cache
(fetchWithCache), TTL 1 hour. Before adding a "refetch" trigger, check
whether the data is already cached — a complete-analysis re-run should not
redundantly refetch repos/contributors if isComplete is already true.
GitHub API errors should always be classified into one of:
- Rate limit
- Network failure
- Authentication (invalid/expired PAT)
- Not Found
- Unknown
Never surface a raw GitHub API error message directly to the user — map it
to one of the categories above and show a clear, actionable message (see
the existing RATE_LIMIT handling in explore() for the pattern to follow).
- Hooks after an early return. Several pages do
if (!model) return nullnear the top. AnyuseMemo/useCallback/custom hook must be called before that line, or React throws "Rendered fewer hooks than expected." - Argument-order mismatches.
fetchRepos(org, repoCount, pat)has been miscalled asfetchRepos(org.login, pat)before, silently breaking PAT detection. Double-check parameter order against the function signature, not just the call site that "looks right." - Clearing state synchronously at the start of a re-fetch.
explore()used to dosetModel(null)immediately on every call, which unmounted pages guarded byif (!model) return nulland caused a visible flicker. Prefer swapping state atomically once new data is ready, so the previous view stays visible during a refresh. - Reusing standard-mode dropdowns/lists without checking what was actually
fetched. Derive UI lists (e.g. repo-select dropdowns) from the fetched
data map (
Object.keys(issuesData)) rather than frommodel.totalRepos, or they'll show entries with no underlying data when in standard mode.
Do not duplicate:
- Analytics formulas (health score, repo ranking, freshness, etc.)
- Fetch logic (
fetchWithCacheand thefetchXfunctions inservices/github.js) - Cache logic (
services/cache.js) - PAT detection/branching (
pat ? ... : ...) - Repository scoping/selection (
selectAnalysisRepos,getTopRepositories)
If you find yourself re-implementing any of the above inline in a page or component, stop and import the existing utility instead. Several bugs in this codebase's history came from exactly this — a feature independently re-deriving repo selection or fetch logic slightly differently than the rest of the app, causing silent data mismatches between pages.
context/AppContext.jsx— all shared state and data-fetching orchestrationservices/github.js— raw GitHub API calls, PAT-aware paginationservices/analytics.js—buildAnalyticalModel,getTopRepositories, time-series buildersservices/cache.js— IndexedDB L2 cache wrappercomponents/AnalysisBanner.jsx— shared standard/complete banner + PAT gatecomponents/PATModal.jsx— PAT entry/save/delete UIcomponents/LearnMoreModal.jsx— explains standard vs. complete analysis limitspages/*.jsx— Overview, Repositories, Contributors, Network, Governance, Analytics, Settings
Target:
- Initial load < 3 seconds
- Cached navigation < 100ms
- Network graph should remain responsive above 1,000 edges
- Zero unnecessary re-renders
Avoid:
- Nested loops over repositories where a single pass would do
- Duplicate API requests for data already in
modelor the IndexedDB cache - Rebuilding graphs/models on every render instead of memoizing
No automated test suite currently exists in this codebase. This section applies once one is introduced.
When it is, every new feature should include:
- Utility/unit tests for pure functions (
services/analytics.js,services/github.jshelpers) - Component tests where behavior is nontrivial (not just presence)
Avoid snapshot-only tests. Prefer tests that assert on actual behavior (e.g. "given these repos and no PAT, the top 10 by score are selected") over tests that just lock in current output.
- Keep PRs focused — avoid unrelated refactors bundled into a feature or fix.
- If you touched fetch logic, re-check the fetch-cap table above stays consistent across contributors/issues/pulls.
- If you touched a page's loading/empty state, check whether a sibling section on the same page has an equivalent state that should match (see the Activity Trends / Advanced Analytics consistency pass in git history for the expected pattern).
- Update this file if the architecture, stable contracts, or metrics formulas change.
- Buttons should have explicit
type="button"unless they're meant to submit a form (rare in this codebase). - Don't hand-roll a new top-N repo cutoff — use
selectAnalysisRepos/getTopRepositories.
Prefer consistency over cleverness.
Reuse existing hooks and components before writing new ones.
Ask before changing core architecture (see Stable Contracts above) rather than assuming a refactor is welcome.