Skip to content

feat: add initial OrgExplorer prototype#63

Merged
Zahnentferner merged 1 commit into
mainfrom
orgexplorer-prototype
May 30, 2026
Merged

feat: add initial OrgExplorer prototype#63
Zahnentferner merged 1 commit into
mainfrom
orgexplorer-prototype

Conversation

@Ri1tik
Copy link
Copy Markdown
Contributor

@Ri1tik Ri1tik commented May 29, 2026

Addressed Issues:

Fixes #

Screenshots/Recordings:

image

Additional Notes:

This PR introduces the initial OrgExplorer prototype focused on organization-level GitHub analytics and governance insights.

Checklist

  • My code follows the project's code style and conventions
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contributing Guidelines

⚠️ AI Notice - Important!

We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added multi-page organization explorer with navigation and routing across overview, repositories, contributors, network, analytics, and governance sections.
    • Integrated GitHub API data fetching with intelligent caching.
    • Added interactive dashboards for repository analytics, contributor intelligence, and governance audits.
    • Added network visualization for contributor-repository relationships.
    • Added CSV export for repositories, contributors, and analytics trends.
    • Added rate-limit monitoring and PAT (Personal Access Token) management interface.
  • Refactor

    • Migrated project from TypeScript to JavaScript.
  • Chores

    • Updated React to 18.3.1 and added routing, charting, and visualization libraries.
    • Simplified build tooling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

<review_stack_artifact>

Walkthrough

This PR converts the OrgExplorer project from TypeScript to JavaScript, removes strict build tooling, and implements a complete React app with GitHub data exploration, analytics dashboards, and governance auditing. New features include organization search, repository/contributor analysis, governance health checks, network visualization, and PAT-based GitHub API integration with IndexedDB caching.

Changes

TypeScript → JavaScript migration and feature implementation

Layer / File(s) Summary
Tooling migration and project config
package.json, index.html, tsconfig*.json, vite.config.js
TypeScript configs removed; package.json downgrades React to 18.3.1 and replaces strict tooling (ESLint/TypeScript) with Vite + React plugin. Dependencies add react-router-dom, recharts, d3, react-icons. Entry point switches from main.tsx to main.jsx.
App routing, context, and entry point
src/App.jsx, src/context/AppContext.jsx, src/main.jsx
App.jsx wires React Router routes to page components and wraps layout with Navbar/RateLimitBanner. AppContext manages PAT state, organization data, analytical model, and GitHub workflows (explore, runAudit). main.jsx mounts the app with StrictMode and BrowserRouter.
GitHub API and analytics services
src/services/github.js, src/services/analytics.js
github.js provides IndexedDB-cached fetch wrappers for organizations, repositories, contributors, issues, and rate limits with 1-hour TTL. analytics.js computes health scores, lifecycles, bus factors, time-series buckets (weekly/monthly), and CSV exports.
Shared UI components, design tokens, sorting hook, and global styling
src/components/UI.jsx, src/hooks/useSortedData.js, src/styles/global.css
UI.jsx exports design tokens (C, LC) and components (Badge, HealthBar, StatCard, Spinner, SortTh, PageTitle, LoadMore, EmptyOk, InfoBox). useSortedData hook manages sort state and memoized sorting. global.css defines CSS reset, theme variables, typography, and animations.
Navigation and rate-limit banner
src/components/Navbar.jsx, src/components/RateLimitBanner.jsx
Navbar renders sticky top navigation with org-conditional route links, wordmark home button, and settings button; shows rate-limit counter when quota is low. RateLimitBanner alerts when quota is critical or missing PAT.
Home page and data exploration
src/pages/HomePage.jsx
HomePage manages multi-org chip input, loads recent searches from localStorage, and triggers explore(orgs) to fetch GitHub data. Shows recent-search and quick-explore shortcuts, plus loading/error states.
Overview and repository pages
src/pages/OverviewPage.jsx, src/pages/RepositoriesPage.jsx
OverviewPage aggregates org stats, displays language distribution and top health-scored repos. RepositoriesPage filters/sorts repositories by lifecycle/language/search, toggles grid/list views, and supports pagination with CSV export.
Analytics and governance audit pages
src/pages/AnalyticsPage.jsx, src/pages/GovernancePage.jsx
AnalyticsPage visualizes PR and issue time-series (weekly/monthly) using recharts AreaCharts. GovernancePage computes dead issues, zombie PRs, risky repos, and unlicensed repos; includes per-repo resolution-rate bars and tabbed detail views.
Contributor intelligence and network visualization
src/pages/ContributorsPage.jsx, src/pages/NetworkPage.jsx
ContributorsPage shows bus-factor risk, freshness metrics, and searchable contributor table with analytics pills. NetworkPage builds D3 force-directed graph of repos and contributors with zoom/pan, drag nodes, recency encoding, and interactive tooltips.
Settings and configuration
src/pages/SettingsPage.jsx
SettingsPage manages GitHub PAT input/visibility, displays live API quota with progress bar, provides cache-clear action, and shows app metadata/stack integrity.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AOSSIE-Org/OrgExplorer#7: Related UI foundation and dependency updates including recharts and package.json changes.
  • AOSSIE-Org/OrgExplorer#2: Builds on initial project scaffold; both PRs update the Vite entrypoint and package configuration.

Suggested labels

feature, React, performance, data-analysis

Suggested reviewers

  • Zahnentferner

Poem

🐰 A rabbit's ode to the refactor:

TypeScript bids farewell, JavaScript takes flight,
Context and services shine oh-so-bright!
From GitHub's API, data flows with grace,
React Router guides each page to its place.
D3 networks dance, analytics ablaze—
The explorer hops forward through data's maze! 🌟

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add initial OrgExplorer prototype' accurately summarizes the main change—introduction of the complete OrgExplorer prototype application with multiple new pages, components, services, and configuration changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch orgexplorer-prototype

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added no-issue-linked PR has no linked issue configuration Config file changes dependencies Dependency updates documentation Documentation updates frontend Frontend changes javascript JavaScript/TypeScript changes size/XL 500+ lines changed first-time-contributor First time contributor labels May 29, 2026
@Ri1tik Ri1tik requested a review from Zahnentferner May 29, 2026 17:52
@github-actions github-actions Bot added size/XL 500+ lines changed and removed size/XL 500+ lines changed labels May 29, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 32

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Around line 6-21: package.json currently removed lint/typecheck scripts and no
CI workflow runs static analysis; restore CI gating by adding npm scripts (e.g.,
"lint" running eslint/biome and "typecheck" running tsc --noEmit or your chosen
type checker) back into the "scripts" object and update your CI workflow to run
those scripts as a job step before merge; reference the existing
eslint.config.js and biome.json to pick the linter command and ensure the
workflow invokes the same commands (or run the appropriate package binary) so
merges always run ESLint/Biome and/or tsc type checks.

In `@src/App.jsx`:
- Around line 21-32: The hardcoded footer strings inside the JSX (the two <span>
contents in the footer element in App.jsx) must be externalized to the app's
i18n/resource system; replace the literal text with references to resource keys
(e.g. t('footer.left') and t('footer.right') or import
strings.footer.left/right) and move the actual strings into the i18n JSON/module
so they can be localized/edited without changing JSX; update any imports (e.g.
useTranslation or strings module) at the top of App.jsx and use the resource
keys in the footer spans.

In `@src/components/Navbar.jsx`:
- Around line 6-13: The LINKS array in Navbar.jsx hardcodes user-visible labels
(e.g., 'Overview', 'Repositories', etc.); replace these literals with i18n
resource lookups by importing your translation function (e.g., t or
useTranslation) and use translated keys (e.g., t('nav.overview')) for each label
in LINKS, and similarly update other hardcoded strings referenced around lines
64-69 to use the same i18n keys; keep the route paths intact and add
corresponding keys to the i18n resource files so translations are available.
- Around line 31-36: Replace the non-interactive clickable wordmark span in
Navbar.jsx (the <span onClick={() => navigate('/')}>ORGEXPLORER</span> usage)
with a semantic, keyboard-accessible element such as a Link (e.g., Link to="/"
from your routing library) or a <button type="button"> that calls navigate; also
move all hardcoded UI strings in this file — the wordmark "ORGEXPLORER", the
labels in the LINKS constant, and the literal "Settings" label — into your app’s
i18n resource/translation files and replace the literals with translation
lookups (e.g., useTranslation/t or your repo’s translation helper) so Navbar
uses translated strings instead of hardcoded text.

In `@src/components/RateLimitBanner.jsx`:
- Around line 27-33: The clickable <span> in RateLimitBanner.jsx that uses
onClick={() => navigate('/settings')} is not keyboard-accessible; replace it
with a semantic control (either a <button type="button"> or a <Link
to="/settings">) and preserve the existing styling and navigate behavior by
using navigate('/settings') in the button's onClick or the Link's to prop;
ensure the element has an accessible label (visible text is fine) and retains
cursor, color and fontWeight styles so keyboard and assistive tech can interact
with it.

In `@src/components/UI.jsx`:
- Around line 106-114: Update Spinner to expose an accessible label prop and
ARIA live/status semantics: add an optional prop (e.g., label or ariaLabel,
defaulting to a localized "Loading..."), and add role="status" and
aria-live="polite" (and include aria-label or visually-hidden text using the
prop) while keeping the visual styles in Spinner({ size = 28, label }). For
SortTh, make the header keyboard-accessible and expose aria-sort: give the
clickable TH a tabIndex={0}, add an onKeyDown handler that triggers the same
action as onClick when Enter or Space is pressed, ensure the element reflects
focus/interaction (e.g., cursor pointer styling), and set aria-sort based on
sortConfig (map to "none", "ascending", or "descending") in the SortTh component
so screen readers see current sort state.
- Around line 122-135: The header cell is currently a clickable <th>
(onClick={() => onSort(sortKey)}) which isn’t keyboard-focusable or announcing
sort state; change the implementation to keep the <th> as a semantic header but
move the interactive handler into a focusable <button> inside it (wrap the
{label} <Icon /> span in a <button type="button"> with onClick={() =>
onSort(sortKey)} and keep visual styles on the button), and add an aria-sort
attribute on the <th> that reflects the current sort state (use the existing
active flag or a sortDirection prop to set aria-sort to "ascending"/"descending"
when active or "none" when not). Ensure the button is keyboard-accessible
(native button covers Enter/Space) and transfer
cursor/userSelect/whiteSpace/font styles from the <th> to the button as needed.

In `@src/context/AppContext.jsx`:
- Around line 8-21: The PAT is currently persisted to localStorage via the pat
state initializer and savePat, which exposes credentials to XSS; change
AppContext so pat is only held in memory: remove reading from localStorage in
the useState initializer for pat (initialize to '' or null) and update savePat
to only call setPat(token) without calling localStorage.setItem/removeItem; if
persistence is absolutely required, add an explicit opt-in API (e.g.,
savePat(token, { persist: true, ttlMs })) that stores a short-lived token with
an expiry mechanism and clearly documents the risk; update any code that expects
localStorage 'oe_pat' accordingly (references to pat, setPat, and savePat).
- Around line 27-28: User-visible status/error strings in AppContext (calls to
setLoadMsg and any direct error copy around the org fetch logic) must be
externalized to the i18n resource files; replace hard-coded strings like
'Fetching organization metadata...', 'No organizations found', 'Error fetching
organization', etc. with keys from your translations and call the localization
helper (e.g. i18n.t or useTranslation().t or a messages map) inside setLoadMsg
and any error state assignments in the same block (look for setLoadMsg and the
error-setting logic around the org fetch Promise.allSettled and the other
occurrences noted). Import the translation helper or messages object at the top
of AppContext.jsx and use it consistently for the other flagged lines (33, 39,
50, 62-64) so all user-visible copy comes from the i18n resources rather than
inline strings.
- Around line 24-68: explore() can be overwritten by out-of-order async results;
add a request-tracking token (e.g., requestIdRef via useRef) that you increment
at the start of explore and capture as a local `currentRequestId`, then before
applying any state updates after awaits (setting orgs, model, rateLimit, error,
loading, loadMsg, and localStorage) verify the local token still equals
requestIdRef.current and bail out if not; also ensure the finally block only
clears loading/loadMsg for the matching request (or resets token on
cancellation). Update the AppContext.jsx to declare the ref and adjust the
asynchronous branches inside explore accordingly.

In `@src/pages/AnalyticsPage.jsx`:
- Around line 53-172: The page currently hardcodes all user-visible strings;
extract text used in PageTitle (title, subtitle), the Export CSV button label,
the granularity button labels (monthly/weekly), the Load Issue and PR History
button text and tooltip states (Fetching...), the empty-state
headings/descriptions, chart headings/labels ("Pull Request Activity", "Issue
Activity", "Created vs Merged vs Closed", "Created vs Closed") and legend names
into i18n resource keys and replace inline literals with i18n lookups (e.g.,
t('analytics.title')); update usages in components like PageTitle, the Export
CSV button (exportTrendsCSV), granularity buttons (setGranularity), runAudit
button, InfoBox sections, and AreaChart name props so all visible strings come
from the i18n resources.
- Around line 29-41: The repo selection currently builds repoNames and filters
in filteredIssues by matching only repo.name which collapses duplicates; change
repoNames to use a stable "org/repo" key (e.g., map to `${r.org}/${r.name}`) and
update the <select> value binding (selectedRepo) to hold that key instead of
just name; then change the filteredIssues logic to directly lookup
issuesData[selectedRepo] (with 'All' still handled) so filtering uses the full
org/repo key and returns the correct array from issuesData, while keeping
allIssues and hasData unchanged.

In `@src/pages/ContributorsPage.jsx`:
- Around line 139-145: In ContributorsPage.jsx inside the visible.map render
(the table row mapping over contributors), add the loading="lazy" attribute to
the avatar <img> element (the one using c.avatar_url and alt={c.login}) so that
images are lazy-loaded; update the <img> JSX in that mapping to include
loading="lazy" to reduce initial network work.
- Around line 26-36: The page initializes sorting with useSortedData(filtered,
'totalContribs', 'desc') which contradicts the subtitle claiming sorting by
coverage and recency; update the default sort to match the UI promise by
changing the useSortedData call in ContributorsPage (the destructured const {
sorted, sortConfig, onSort } = useSortedData(...)) to use the coverage metric as
the primary sort key (e.g., 'coverage' or your coverage field name) and
descending order, and if useSortedData supports a secondary key include recency
(e.g., 'lastActive' or 'recency') as the tiebreaker; alternatively, if you
cannot change logic, update the subtitle text to reflect the actual default
('totalContribs') instead.

In `@src/pages/GovernancePage.jsx`:
- Around line 32-40: The deadIssues and zombiePRs lists currently only filter by
type and age, so closed issues and merged/closed PRs are included; update both
filters on the variables deadIssues and zombiePRs (operating on allIssues) to
also require the item to be open (e.g., i.state === 'open') so only open
issues/PRs older than 90 days are counted; you can add the open-state check
inside the existing .filter callbacks that reference i.pull_request, daysSince,
and created_at or add a separate .filter(i => i.state === 'open') before
sorting.
- Around line 136-149: The issue is that repoIssues is built by filtering
allIssues only by i.repoName === r.name in the topRepos.map block, which merges
repos with the same name across orgs; change the predicate in the repoIssues
filter to include the org as well (e.g. i.repoName === r.name && i.orgLogin ===
r.orgLogin) so the computed closed/total/rate and color are per full repo slug;
update the filter inside the topRepos.map where repoIssues is declared.

In `@src/pages/HomePage.jsx`:
- Around line 50-90: The JSX in HomePage.jsx embeds user-visible strings
directly (hero title/span "Insights", hero paragraph, input placeholders 'Add
another org...' and 'AOSSIE-Org, ...', help text, button label
"EXPLORE")—externalize them to your i18n resources and use the app's translation
helper (e.g., useTranslation()/t) instead of literals. Replace each literal in
the HomePage component (search container, <h1>, the <span> with color, the
descriptive <p>, input placeholder, the small help <p>, and the button label
used by go()) with t('home.hero.title'), t('home.hero.accent'),
t('home.hero.description'), t('home.search.addAnother'),
t('home.search.placeholder'), t('home.search.help'), t('home.button.explore')
(or your existing key naming convention), add those keys to the locale resource
files, and ensure functions referenced here (chips, addChip, handleKey, go,
C.btn) continue to work unchanged.
- Around line 67-75: The chip remove control is currently an interactive icon
inside a non-interactive span (chips.map rendering) which prevents keyboard
focus and screen-reader labeling; replace the inline <span> remove affordance
with a real <button> (or make the element a button element) for each chip, move
the onClick handler from FiX to that button (keeping removeChip(c)), give the
button type="button" and a clear accessible name via aria-label={`Remove ${c}`}
(or visually hidden text) and preserve the current styles (or apply them to the
button) so appearance is unchanged while enabling keyboard focus and
screen-reader support.
- Line 15: The HomePage.jsx currently parses localStorage.getItem('oe_recent')
without guarding against malformed JSON, renders the chip remove icon as a
non-focusable FiX element, and hardcodes visible copy; fix by wrapping the
parsing of oe_recent in a try/catch and falling back to [] (update the const
recent initialization near its declaration), replace the chip remove FiX element
with an actual <button type="button"> that includes aria-label (e.g., "Remove
{term}") and onClick handler so it is keyboard-accessible (update the chip
render code where FiX and its onClick are used), and extract all user-visible
strings (hero text, EXPLORE, "Recent searches", placeholder, etc.) into i18n
resource keys and reference them instead of hardcoded literals in the JSX
(replace inline strings in HomePage.jsx with i18n lookups).

In `@src/pages/NetworkPage.jsx`:
- Around line 31-39: Repository nodes and link targets currently use
`repo:${r.name}` which collapses identically-named repos across orgs; update
creation of repo node IDs in the `nodes` array and the link target generation in
the `topContribs.forEach` block to use the repo slug (org/name) instead of just
the name. Specifically, when pushing repo nodes (in the `showRepos` /
`topRepos.forEach` code that references `r`) build the id as
`repo:${r.full_name}` (or if `full_name` is not present, compose
`repo:${r.owner.login}/${r.name}`), and in the contributor link loop use the
same slug for `t` when referencing `repo` (i.e., `t = 'repo:' + (repo.full_name
|| repo.owner.login + '/' + repo.name)`) so `nodeSet.has` and `links.push`
reference the exact same slugged IDs.

In `@src/pages/OverviewPage.jsx`:
- Around line 87-91: The active percentage calculation can divide by zero when
allRepos.length is 0, producing "NaN% of total"; update the rendering where
StatCard for "Active Repos" is created (the allRepos, activeRepos usage in
OverviewPage.jsx) to guard the denominator by computing percentage only if
allRepos.length > 0, otherwise use "0%" (e.g., const pct = allRepos.length ?
Math.round(activeRepos / allRepos.length * 100) : 0 and pass `${pct}% of total`
as the sub prop).
- Around line 28-40: NavCard is currently a non-focusable clickable div: change
the outer element to a semantic <button> (ensure type="button") so it is
keyboard-focusable/activatable, keep the onClick handler and transfer
onMouseEnter/onMouseLeave to onFocus/onBlur as needed and preserve the inline
styles and role for visual behavior; also ensure an accessible label (e.g., use
aria-label or keep visible label). For the “Active Repos” percent calculation
that uses allRepos.length, guard against division by zero by returning 0 (or an
appropriate fallback) when allRepos.length === 0 instead of computing the
percentage (e.g., use a ternary like allRepos.length ? (activeCount /
allRepos.length) * 100 : 0).

In `@src/pages/RepositoriesPage.jsx`:
- Around line 99-185: The UI currently renders an empty table/grid when the
filtered list is empty (visible.length === 0) and still shows LoadMore, which is
confusing; update the RepositoriesPage render logic to detect when visible is
empty and show a dedicated empty state message/component (e.g., "No repositories
match your filters") instead of the table/grid, and do not render the LoadMore
in that branch. Locate the conditional blocks for view === 'list' and view ===
'grid' (they iterate over visible and call LoadMore and setShown), add a
top-level check for visible.length === 0 to render the empty state (reusing
C.card styling and a simple message or placeholder component) and return early
or skip rendering LoadMore and the list/grid markup when empty.

In `@src/pages/SettingsPage.jsx`:
- Around line 68-73: The PAT visibility toggle currently lacks an accessible
name and the refresh icon is a non-interactive element styled as clickable;
update the visibility control (the button using setShow and icons
FiEye/FiEyeOff) to include an explicit accessible name (aria-label or
aria-labelledby and optional title) that reflects its action (e.g., "Toggle PAT
visibility"), and ensure the refresh icon at the other location is converted
into a real interactive element (use a <button> or a focusable element with
role="button", keyboard handlers, and an accessible name) instead of a styled
<span>/icon-only element; confirm both controls have visible focus styles and
appropriate aria-pressed/state if needed.
- Around line 37-195: The component hardcodes user-visible strings (titles,
labels, placeholders, button text and hints) which must be externalized for
i18n; refactor SettingsPage.jsx to move those literals into your i18n resource
files and replace them with translation lookups (e.g. using
useTranslation()/t('settings.github.title') or your app's i18n helper). Replace
texts found in the "GitHub Authentication" card (e.g. "GitHub Authentication",
"Personal Access Token (PAT)", "PRIVATE TOKEN KEY", placeholder "ghp_xxx",
button labels Saved/Save/Delete), the "How to create a PAT" steps, the "Data
Cache" card text and button ("Data Cache", "Clear All"/"Cleared", description),
the "API Quota" messages (including "API Quota", remaining/limit labels and the
note about adding a PAT), and the "Architect Meta" section entries ("Architect
Meta", "Stack Integrity" and the list items). Keep existing symbols/handlers
(draft, pat, show, handleSave, handleDelete, handleClear, rateLimit, rateColor,
C.card) and only swap static strings for t('...') keys (suggest a consistent key
namespace like settings.* and descriptive keys for each literal), then add
corresponding entries to the locale JSON/resource files.
- Around line 25-29: handleClear always sets cleared to true even when
cacheClear failed (cacheClear returns false on error). Change handleClear to
await the boolean result from cacheClear and only call setCleared(true) when
that result is truthy; if cacheClear returns false, avoid setting cleared and
optionally handle/log the failure. Keep the existing setTimeout(() =>
setCleared(false), 2000) but only schedule it when you actually set cleared to
true so the UI won’t show “Cleared” on failure.
- Around line 31-33: The rate color and progress calculations perform unsafe
division by rateLimit.limit; change both usages to compute a guarded percentage
(e.g., let safePct = (rateLimit && Number(rateLimit.limit)) ?
Number(rateLimit.remaining) / Number(rateLimit.limit) : 0), clamp safePct to
[0,1], then use safePct for the ternary that sets rateColor (replace the direct
rateLimit.remaining/rateLimit.limit expression) and for the progress bar width
(use safePct*100 + '%'); apply the same pattern in RateLimitBanner.jsx where
const pct is computed to avoid NaN/Infinity when limit is 0 or missing.

In `@src/services/analytics.js`:
- Around line 22-34: computeBusFactor currently assumes contributors are
pre-sorted; sort the contributors array by contributions in descending order at
the start of computeBusFactor(contributors = []) before computing
total/cumulative values so the algorithm finds the smallest dominant set
reliably. Use a numeric descending sort on the contributions property (operate
on a shallow copy to avoid mutating the original input) and then proceed with
the existing total, cumulative loop and risk logic in computeBusFactor.
- Around line 146-169: The CSV exporters (exportReposCSV, exportContributorsCSV,
exportTrendsCSV) currently join fields with commas and can break when values
contain commas, quotes or newlines; add a helper (e.g., escapeCsvCell) and use
it in download and each exporter to wrap fields in double quotes and double any
internal quotes (""), convert null/undefined to empty string, and then join
escaped fields with commas and rows with newlines before creating the Blob in
download so generated CSVs are properly quoted and safe.

In `@src/services/github.js`:
- Around line 88-101: fetchContributors and fetchIssues currently fetch a single
page (30 and 100 items) and swallow all errors into empty arrays; update both
functions (fetchContributors, fetchIssues) to paginate the GitHub API using the
per_page and page query params (or follow the Link header) to accumulate all
pages until no more results, and stop using a blanket catch that returns [];
instead propagate errors (rethrow) or return an error result so callers can
handle rate-limits/auth/network failures rather than treating them as “no data”.
Ensure you aggregate results across pages and preserve existing use of
fetchWithCache/pat while adding pagination and proper error propagation.
- Around line 55-69: fetchWithCache currently uses the raw URL as the cache key
which can mix responses across different auth contexts; modify fetchWithCache to
include auth context in the cache key (or skip caching for authenticated
requests) by deriving a cacheKey from the URL plus an auth marker (e.g., "anon"
vs a stable identifier for the PAT such as a short hash) before calling
cacheGet/cacheSet, and only call cacheSet when the response is being cached for
the matching auth context; update references to cacheGet(url)/cacheSet(url,
data) to use cacheKey and ensure authenticated responses are either separately
namespaced or not cached.

In `@src/styles/global.css`:
- Around line 41-50: The animations defined by `@keyframes` spin and fadeUp and
used by .fade-up run unconditionally; add a prefers-reduced-motion override so
users who opt out of motion get no animations: create a `@media`
(prefers-reduced-motion: reduce) block that sets animation: none (or
animation-duration: 0s) and removes transform/transition effects for .fade-up
and any element relying on spin (refer to `@keyframes` spin and the spinner
class/usage), ensuring the existing keyframes stay but are not applied when
motion is reduced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a33eff5b-9849-4b93-9bd9-08afe9400346

📥 Commits

Reviewing files that changed from the base of the PR and between dc8a988 and de6709d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • Tasks.md
  • index.html
  • package.json
  • src/App.css
  • src/App.jsx
  • src/App.tsx
  • src/components/Navbar.jsx
  • src/components/RateLimitBanner.jsx
  • src/components/UI.jsx
  • src/context/AppContext.jsx
  • src/hooks/useSortedData.js
  • src/index.css
  • src/main.jsx
  • src/main.tsx
  • src/pages/AnalyticsPage.jsx
  • src/pages/ContributorsPage.jsx
  • src/pages/GovernancePage.jsx
  • src/pages/HomePage.jsx
  • src/pages/NetworkPage.jsx
  • src/pages/OverviewPage.jsx
  • src/pages/RepositoriesPage.jsx
  • src/pages/SettingsPage.jsx
  • src/services/analytics.js
  • src/services/github.js
  • src/styles/global.css
  • tsconfig.app.json
  • tsconfig.json
  • tsconfig.node.json
  • vite.config.js
💤 Files with no reviewable changes (8)
  • vite.config.js
  • src/index.css
  • tsconfig.app.json
  • tsconfig.json
  • src/App.tsx
  • src/main.tsx
  • src/App.css
  • tsconfig.node.json

Comment thread package.json
Comment on lines 6 to +21
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"recharts": "^2.12.7",
"d3": "^7.9.0",
"react-icons": "^5.3.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "npm:rolldown-vite@7.2.5"
},
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.6"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package scripts =="
sed -n '/"scripts": {/,/}/p' package.json

echo
echo "== lint / static-analysis config files =="
fd -HI '^(eslint\.config\.(js|cjs|mjs)|\.eslintrc(\.(js|cjs|json|yaml|yml))?|biome\.json|oxlintrc\.(json|js)|tsconfig(\..*)?\.json)$'

echo
echo "== workflow references to lint or type checks =="
rg -n --hidden -g '.github/workflows/*' -e '\blint\b|eslint|biome|oxlint|typecheck|tsc'

Repository: AOSSIE-Org/OrgExplorer

Length of output: 305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .github/workflows files =="
ls -la .github/workflows || true
echo

for f in .github/workflows/*; do
  [ -f "$f" ] || continue
  echo "---- $f ----"
  # case-insensitive search for common static-analysis/type-check commands
  rg -n -i '\bleslint\b|biome|oxlint|typecheck|tsc\b|typescript\b|pnpm\s+lint|npm\s+run\s+lint|yarn\s+lint' "$f" || true
done

echo
echo "== package.json scripts (full) =="
cat package.json | sed -n '1,120p'

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1303


Restore an equivalent lint/static-analysis CI gate
package.json now has only dev/build/preview scripts (no lint), and none of the .github/workflows/* run lint/type-check steps (no matches for lint, eslint, biome, tsc, or typecheck). Even though eslint.config.js and biome.json still exist, they won’t protect merges. Re-add a CI job (or restore the script wiring) to run ESLint/Biome and/or a type-check before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 6 - 21, package.json currently removed
lint/typecheck scripts and no CI workflow runs static analysis; restore CI
gating by adding npm scripts (e.g., "lint" running eslint/biome and "typecheck"
running tsc --noEmit or your chosen type checker) back into the "scripts" object
and update your CI workflow to run those scripts as a job step before merge;
reference the existing eslint.config.js and biome.json to pick the linter
command and ensure the workflow invokes the same commands (or run the
appropriate package binary) so merges always run ESLint/Biome and/or tsc type
checks.

Comment thread src/App.jsx
Comment on lines +21 to +32
<footer style={{
padding: '18px 24px',
borderTop: '1px solid var(--border)',
display: 'flex',
justifyContent: 'space-between',
fontSize: 11,
color: 'var(--text3)',
letterSpacing: '.04em',
marginTop: 40,
}}>
<span>V2.0.0 · GITHUB · DOCUMENTATION · TERMS</span>
<span>2026 ORGEXPLORER · BUILT BY RITIK</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Externalize the footer copy.

These labels are user-visible strings baked directly into JSX, which will make localization and copy changes harder once the rest of the app grows.

As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.jsx` around lines 21 - 32, The hardcoded footer strings inside the
JSX (the two <span> contents in the footer element in App.jsx) must be
externalized to the app's i18n/resource system; replace the literal text with
references to resource keys (e.g. t('footer.left') and t('footer.right') or
import strings.footer.left/right) and move the actual strings into the i18n
JSON/module so they can be localized/edited without changing JSX; update any
imports (e.g. useTranslation or strings module) at the top of App.jsx and use
the resource keys in the footer spans.

Comment thread src/components/Navbar.jsx
Comment on lines +6 to +13
const LINKS = [
{ to: '/overview', label: 'Overview' },
{ to: '/repositories', label: 'Repositories' },
{ to: '/contributors', label: 'Contributors' },
{ to: '/network', label: 'Network' },
{ to: '/analytics', label: 'Analytics' },
{ to: '/governance', label: 'Governance' },
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Externalize navigation copy instead of hardcoding it here.

Overview, Repositories, Settings, and the other visible labels should come from your i18n resources rather than being embedded in the component.

As per coding guidelines, "Internationalization: - User-visible strings should be externalized to resource files (i18n)".

Also applies to: 64-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Navbar.jsx` around lines 6 - 13, The LINKS array in Navbar.jsx
hardcodes user-visible labels (e.g., 'Overview', 'Repositories', etc.); replace
these literals with i18n resource lookups by importing your translation function
(e.g., t or useTranslation) and use translated keys (e.g., t('nav.overview'))
for each label in LINKS, and similarly update other hardcoded strings referenced
around lines 64-69 to use the same i18n keys; keep the route paths intact and
add corresponding keys to the i18n resource files so translations are available.

Comment thread src/components/Navbar.jsx
Comment on lines +31 to +36
<span
onClick={() => navigate('/')}
style={{ fontWeight: 800, fontSize: 15, letterSpacing: '.1em', color: 'var(--accent)', cursor: 'pointer', flexShrink: 0 }}
>
ORGEXPLORER
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the targeted file around the referenced lines
if [ -f "src/components/Navbar.jsx" ]; then
  echo "=== File: src/components/Navbar.jsx ==="
  nl -ba src/components/Navbar.jsx | sed -n '1,140p'
else
  echo "Missing: src/components/Navbar.jsx"
fi

# Check whether the repo uses any i18n pattern (common libraries/keywords)
echo "=== i18n search (quick) ==="
rg -n --hidden --glob '!.next' --glob '!node_modules' \
  -e "useTranslation\(" -e "next-intl" -e "react-i18next" -e "i18n" -e "t\(" \
  src || true

# Check for "use client" usage in the file and similar components
echo "=== 'use client' search around components ==="
rg -n --hidden --glob '!.next' --glob '!node_modules' -g'*.jsx' "use client" src/components || true

# Search for other hardcoded nav labels in Navbar.jsx
echo "=== Navbar.jsx user-visible strings (heuristic) ==="
rg -n "ORGEXPLORER|Settings|Navigation|Home|Settings" src/components/Navbar.jsx || true

Repository: AOSSIE-Org/OrgExplorer

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Locate Navbar files ==="
fd -a -i "navbar" src || true

# Prefer exact path if it exists
TARGET="src/components/Navbar.jsx"
if [ ! -f "$TARGET" ]; then
  # Try common alternatives
  for alt in "src/components/Navbar.tsx" "src/components/navbar.jsx" "src/components/navbar.tsx"; do
    if [ -f "$alt" ]; then TARGET="$alt"; break; fi
  done
fi

echo "=== Using target: $TARGET ==="
if [ -f "$TARGET" ]; then
  echo "=== File contents (first 180 lines with line numbers) ==="
  awk '{printf "%6d\t%s\n", NR, $0}' "$TARGET" | sed -n '1,180p'
else
  echo "Missing navbar file at expected paths; cannot verify snippet."
fi

echo "=== i18n search (quick) ==="
rg -n --hidden --glob '!.next' --glob '!node_modules' \
  -e "useTranslation\(" -e "next-intl" -e "react-i18next" -e "i18n" -e "t\(" src || true

echo "=== Hardcoded nav-ish strings in navbar components ==="
if [ -f "$TARGET" ]; then
  rg -n "ORGEXPLORER|Settings|Navigation|Home" "$TARGET" || true
fi

echo "=== Keyboard/accessibility hints search in repo ==="
rg -n --hidden --glob '!.next' --glob '!node_modules' \
  -e "aria-label" -e "role=" -e "tabIndex=" -e "onKeyDown" -e "<button" -e "<Link" src/components || true

Repository: AOSSIE-Org/OrgExplorer

Length of output: 10011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for i18n infrastructure ==="
rg -n --hidden --glob '!.next' --glob '!node_modules' \
  -e "react-intl" -e "next-intl" -e "i18next" -e "react-i18next" -e "useTranslation\(" -e "t\(" \
  src || true

echo "=== Search for locale/translation resource files ==="
fd -a -i "locales" . || true
fd -a -i "i18n" . || true
fd -a -i "translation" . || true
fd -a -i "messages" . || true

echo "=== Search for any custom translation helper ==="
rg -n --hidden --glob '!.next' --glob '!node_modules' \
  -e "translate" -e "messages" -e "getMessage" -e "locale" \
  src || true

echo "=== Check if Navbar strings are ever passed through a translator ==="
rg -n "ORGEXPLORER|Settings|Overview|Repositories|Contributors|Network|Analytics|Governance" src/components/Navbar.jsx src/pages || true

Repository: AOSSIE-Org/OrgExplorer

Length of output: 9988


Replace the mouse-only home wordmark and externalize Navbar copy

  • In src/components/Navbar.jsx (lines 31-36), change the clickable <span onClick={...}>ORGEXPLORER</span> to a semantic <Link to="/" /> (or <button type="button">) so it’s keyboard and assistive-tech accessible.
  • Hardcoded labels (ORGEXPLORER, LINKS labels, and “Settings”) should be moved into the repo’s i18n/translation resources instead of literal strings.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Navbar.jsx` around lines 31 - 36, Replace the non-interactive
clickable wordmark span in Navbar.jsx (the <span onClick={() =>
navigate('/')}>ORGEXPLORER</span> usage) with a semantic, keyboard-accessible
element such as a Link (e.g., Link to="/" from your routing library) or a
<button type="button"> that calls navigate; also move all hardcoded UI strings
in this file — the wordmark "ORGEXPLORER", the labels in the LINKS constant, and
the literal "Settings" label — into your app’s i18n resource/translation files
and replace the literals with translation lookups (e.g., useTranslation/t or
your repo’s translation helper) so Navbar uses translated strings instead of
hardcoded text.

Comment on lines +27 to +33
{!pat && (
<span
onClick={() => navigate('/settings')}
style={{ color: 'var(--accent)', marginLeft: 10, cursor: 'pointer', fontWeight: 600 }}
>
Add PAT for 5,000 req/hr
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant portion of the file
if [ -f "src/components/RateLimitBanner.jsx" ]; then
  echo "== File: src/components/RateLimitBanner.jsx =="
  nl -ba src/components/RateLimitBanner.jsx | sed -n '1,120p'
else
  echo "File not found: src/components/RateLimitBanner.jsx"
fi

# Search for the specific CTA text and for any accessibility props on the span
echo "== Grep for CTA text =="
rg -n "Add PAT for 5,000 req/hr" src/components/RateLimitBanner.jsx || true

echo "== Grep for span/button/link usage near navigate('/settings') =="
rg -n "navigate\\('/settings'\\)|onClick=\\{\\(\\) => navigate\\('/settings'\\)\\}|Add PAT|role=|tabIndex=|onKeyDown=" src/components/RateLimitBanner.jsx || true

Repository: AOSSIE-Org/OrgExplorer

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/RateLimitBanner.jsx"

echo "== Existence check =="
ls -la "$FILE" || true

echo
echo "== Snippet around lines 20-50 (no nl available) =="
if [ -f "$FILE" ]; then
  # Print with line numbers using awk
  awk 'NR>=20 && NR<=50 {printf "%5d:%s\n", NR, $0}' "$FILE"
fi

echo
echo "== Grep for CTA text =="
rg -n "Add PAT for 5,000 req/hr" "$FILE" || true

echo
echo "== Grep for navigate('/settings') usage in this file =="
rg -n "navigate\\('/settings'\\)" "$FILE" || true

echo
echo "== Grep for accessibility-related props on the CTA element =="
rg -n "role=|tabIndex=|onKeyDown=|onKeyUp=|onKeyPress=|aria-" "$FILE" || true

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/RateLimitBanner.jsx"

echo "== Top of file =="
head -n 80 "$FILE"

echo
echo "== Search for use client =="
rg -n "use client" "$FILE" || true

echo
echo "== Search for navigate() definition/import =="
rg -n "navigate\\b|useNavigate\\b|next/navigation|react-router" "$FILE" || true

echo
echo "== Re-check CTA element props =="
rg -n "onClick=\\{\\(\\) => navigate\\('/settings'\\)\\}|onClick=\\(\\) => navigate\\('/settings'\\)|Add PAT for 5,000 req/hr" "$FILE"

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1872


Make the PAT CTA keyboard-accessible (use a button/link)

src/components/RateLimitBanner.jsx renders the “Add PAT for 5,000 req/hr” control as a clickable <span> (onClick={() => navigate('/settings')}) with no semantic role or keyboard support, so it won’t be reachable/announced correctly for keyboard and assistive technologies. Render it as a real <button type="button"> or a <Link> to /settings while keeping the same styling/behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/RateLimitBanner.jsx` around lines 27 - 33, The clickable
<span> in RateLimitBanner.jsx that uses onClick={() => navigate('/settings')} is
not keyboard-accessible; replace it with a semantic control (either a <button
type="button"> or a <Link to="/settings">) and preserve the existing styling and
navigate behavior by using navigate('/settings') in the button's onClick or the
Link's to prop; ensure the element has an accessible label (visible text is
fine) and retains cursor, color and fontWeight styles so keyboard and assistive
tech can interact with it.

Comment thread src/services/analytics.js
Comment on lines +22 to +34
export function computeBusFactor(contributors = []) {
if (!contributors.length) return { factor: 0, risk: 'unknown' }
const total = contributors.reduce((s, c) => s + c.contributions, 0)
if (!total) return { factor: 0, risk: 'unknown' }
let cum = 0
for (let i = 0; i < contributors.length; i++) {
cum += contributors[i].contributions
if (cum / total > 0.5) {
const f = i + 1
return { factor: f, risk: f <= 1 ? 'critical' : f <= 2 ? 'high' : 'healthy' }
}
}
return { factor: contributors.length, risk: 'healthy' }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sort contributors before computing the bus factor.

This only returns the smallest dominant set if contributors is already ordered by descending contributions. Since this helper is exported, that assumption should be enforced here rather than pushed to every caller.

Suggested fix
 export function computeBusFactor(contributors = []) {
   if (!contributors.length) return { factor: 0, risk: 'unknown' }
-  const total = contributors.reduce((s, c) => s + c.contributions, 0)
+  const ranked = [...contributors].sort((a, b) => b.contributions - a.contributions)
+  const total = ranked.reduce((s, c) => s + c.contributions, 0)
   if (!total) return { factor: 0, risk: 'unknown' }
   let cum = 0
-  for (let i = 0; i < contributors.length; i++) {
-    cum += contributors[i].contributions
+  for (let i = 0; i < ranked.length; i++) {
+    cum += ranked[i].contributions
     if (cum / total > 0.5) {
       const f = i + 1
       return { factor: f, risk: f <= 1 ? 'critical' : f <= 2 ? 'high' : 'healthy' }
     }
   }
-  return { factor: contributors.length, risk: 'healthy' }
+  return { factor: ranked.length, risk: 'healthy' }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/analytics.js` around lines 22 - 34, computeBusFactor currently
assumes contributors are pre-sorted; sort the contributors array by
contributions in descending order at the start of computeBusFactor(contributors
= []) before computing total/cumulative values so the algorithm finds the
smallest dominant set reliably. Use a numeric descending sort on the
contributions property (operate on a shallow copy to avoid mutating the original
input) and then proceed with the existing total, cumulative loop and risk logic
in computeBusFactor.

Comment thread src/services/analytics.js
Comment on lines +146 to +169
function download(content, filename, type = 'text/csv') {
const blob = new Blob([content], { type })
const url = URL.createObjectURL(blob)
const a = Object.assign(document.createElement('a'), { href: url, download: filename })
a.click()
URL.revokeObjectURL(url)
}

export function exportReposCSV(repos) {
const header = ['Repository','Org','Stars','Forks','Open Issues','Health Score','Lifecycle','Language','Last Active']
const rows = repos.map(r => [r.name, r.orgLogin, r.stargazers_count, r.forks_count, r.open_issues_count, r.healthScore, r.lifecycle, r.language || 'N/A', r.pushed_at?.slice(0, 10)])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-repos.csv')
}

export function exportContributorsCSV(contributors) {
const header = ['Login','Total Contributions','Repos','Orgs','Last Active','Connector','Cross-Org']
const rows = contributors.map(c => [c.login, c.totalContribs, c.repos.length, c.orgs.length, c.lastActive?.slice(0, 10) || '', c.isConnector, c.isCrossOrg])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-contributors.csv')
}

export function exportTrendsCSV(series) {
const header = ['Date','PRs Created','PRs Merged','PRs Closed','Issues Created','Issues Closed']
const rows = series.map(s => [s.date, s.prs_created, s.prs_merged, s.prs_closed, s.issues_created, s.issues_closed])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-trends.csv')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape CSV cells before joining rows.

These exporters write raw values with join(','), so any comma, quote, or newline inside a field will produce a malformed CSV. Repository names, languages, and future labels are all user-controlled enough to hit this.

Suggested fix
+function csvCell(value) {
+  const s = String(value ?? '')
+  return /[",\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s
+}
+
 function download(content, filename, type = 'text/csv') {
@@
 export function exportReposCSV(repos) {
   const header = ['Repository','Org','Stars','Forks','Open Issues','Health Score','Lifecycle','Language','Last Active']
   const rows   = repos.map(r => [r.name, r.orgLogin, r.stargazers_count, r.forks_count, r.open_issues_count, r.healthScore, r.lifecycle, r.language || 'N/A', r.pushed_at?.slice(0, 10)])
-  download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-repos.csv')
+  download([header, ...rows].map(r => r.map(csvCell).join(',')).join('\n'), 'orgexplorer-repos.csv')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function download(content, filename, type = 'text/csv') {
const blob = new Blob([content], { type })
const url = URL.createObjectURL(blob)
const a = Object.assign(document.createElement('a'), { href: url, download: filename })
a.click()
URL.revokeObjectURL(url)
}
export function exportReposCSV(repos) {
const header = ['Repository','Org','Stars','Forks','Open Issues','Health Score','Lifecycle','Language','Last Active']
const rows = repos.map(r => [r.name, r.orgLogin, r.stargazers_count, r.forks_count, r.open_issues_count, r.healthScore, r.lifecycle, r.language || 'N/A', r.pushed_at?.slice(0, 10)])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-repos.csv')
}
export function exportContributorsCSV(contributors) {
const header = ['Login','Total Contributions','Repos','Orgs','Last Active','Connector','Cross-Org']
const rows = contributors.map(c => [c.login, c.totalContribs, c.repos.length, c.orgs.length, c.lastActive?.slice(0, 10) || '', c.isConnector, c.isCrossOrg])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-contributors.csv')
}
export function exportTrendsCSV(series) {
const header = ['Date','PRs Created','PRs Merged','PRs Closed','Issues Created','Issues Closed']
const rows = series.map(s => [s.date, s.prs_created, s.prs_merged, s.prs_closed, s.issues_created, s.issues_closed])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-trends.csv')
function csvCell(value) {
const s = String(value ?? '')
return /[",\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s
}
function download(content, filename, type = 'text/csv') {
const blob = new Blob([content], { type })
const url = URL.createObjectURL(blob)
const a = Object.assign(document.createElement('a'), { href: url, download: filename })
a.click()
URL.revokeObjectURL(url)
}
export function exportReposCSV(repos) {
const header = ['Repository','Org','Stars','Forks','Open Issues','Health Score','Lifecycle','Language','Last Active']
const rows = repos.map(r => [r.name, r.orgLogin, r.stargazers_count, r.forks_count, r.open_issues_count, r.healthScore, r.lifecycle, r.language || 'N/A', r.pushed_at?.slice(0, 10)])
download([header, ...rows].map(r => r.map(csvCell).join(',')).join('\n'), 'orgexplorer-repos.csv')
}
export function exportContributorsCSV(contributors) {
const header = ['Login','Total Contributions','Repos','Orgs','Last Active','Connector','Cross-Org']
const rows = contributors.map(c => [c.login, c.totalContribs, c.repos.length, c.orgs.length, c.lastActive?.slice(0, 10) || '', c.isConnector, c.isCrossOrg])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-contributors.csv')
}
export function exportTrendsCSV(series) {
const header = ['Date','PRs Created','PRs Merged','PRs Closed','Issues Created','Issues Closed']
const rows = series.map(s => [s.date, s.prs_created, s.prs_merged, s.prs_closed, s.issues_created, s.issues_closed])
download([header, ...rows].map(r => r.join(',')).join('\n'), 'orgexplorer-trends.csv')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/analytics.js` around lines 146 - 169, The CSV exporters
(exportReposCSV, exportContributorsCSV, exportTrendsCSV) currently join fields
with commas and can break when values contain commas, quotes or newlines; add a
helper (e.g., escapeCsvCell) and use it in download and each exporter to wrap
fields in double quotes and double any internal quotes (""), convert
null/undefined to empty string, and then join escaped fields with commas and
rows with newlines before creating the Blob in download so generated CSVs are
properly quoted and safe.

Comment thread src/services/github.js
Comment on lines +55 to +69
async function fetchWithCache(url, pat) {
// L2 check
const cached = await cacheGet(url)
if (cached) return cached

const headers = { Accept: 'application/vnd.github.v3+json' }
if (pat) headers.Authorization = `token ${pat}`

const res = await fetch(url, { headers })
if (res.status === 403) throw new Error('RATE_LIMIT')
if (res.status === 404) throw new Error('NOT_FOUND')
if (!res.ok) throw new Error(`HTTP_${res.status}`)

const data = await res.json()
cacheSet(url, data) // write-back, non-blocking
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope the cache by auth context or skip caching authenticated responses.

Using the raw URL as the cache key lets data fetched with one PAT be served back after the PAT changes, or even after switching to anonymous mode. That mixes permission scopes in IndexedDB and can expose stale private/org-specific data within the same browser profile.

Suggested fix
 async function fetchWithCache(url, pat) {
-  // L2 check
-  const cached = await cacheGet(url)
+  const cacheKey = pat ? null : url
+  const cached = cacheKey ? await cacheGet(cacheKey) : null
   if (cached) return cached
@@
   const data = await res.json()
-  cacheSet(url, data) // write-back, non-blocking
+  if (cacheKey) void cacheSet(cacheKey, data)
   return data
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function fetchWithCache(url, pat) {
// L2 check
const cached = await cacheGet(url)
if (cached) return cached
const headers = { Accept: 'application/vnd.github.v3+json' }
if (pat) headers.Authorization = `token ${pat}`
const res = await fetch(url, { headers })
if (res.status === 403) throw new Error('RATE_LIMIT')
if (res.status === 404) throw new Error('NOT_FOUND')
if (!res.ok) throw new Error(`HTTP_${res.status}`)
const data = await res.json()
cacheSet(url, data) // write-back, non-blocking
async function fetchWithCache(url, pat) {
const cacheKey = pat ? null : url
const cached = cacheKey ? await cacheGet(cacheKey) : null
if (cached) return cached
const headers = { Accept: 'application/vnd.github.v3+json' }
if (pat) headers.Authorization = `token ${pat}`
const res = await fetch(url, { headers })
if (res.status === 403) throw new Error('RATE_LIMIT')
if (res.status === 404) throw new Error('NOT_FOUND')
if (!res.ok) throw new Error(`HTTP_${res.status}`)
const data = await res.json()
if (cacheKey) void cacheSet(cacheKey, data)
return data
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/github.js` around lines 55 - 69, fetchWithCache currently uses
the raw URL as the cache key which can mix responses across different auth
contexts; modify fetchWithCache to include auth context in the cache key (or
skip caching for authenticated requests) by deriving a cacheKey from the URL
plus an auth marker (e.g., "anon" vs a stable identifier for the PAT such as a
short hash) before calling cacheGet/cacheSet, and only call cacheSet when the
response is being cached for the matching auth context; update references to
cacheGet(url)/cacheSet(url, data) to use cacheKey and ensure authenticated
responses are either separately namespaced or not cached.

Comment thread src/services/github.js
Comment on lines +88 to +101
export async function fetchContributors(org, repo, pat) {
try {
return await fetchWithCache(
`https://api.github.com/repos/${org}/${repo}/contributors?per_page=30`, pat
)
} catch { return [] }
}

export async function fetchIssues(org, repo, pat) {
try {
return await fetchWithCache(
`https://api.github.com/repos/${org}/${repo}/issues?state=all&per_page=100`, pat
)
} catch { return [] }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Paginate these collection endpoints and stop collapsing failures into empty arrays.

fetchContributors caps each repo at 30 contributors and fetchIssues caps at 100 items, so larger repos get systematically undercounted. On top of that, the blanket catch { return [] } turns rate-limit/auth/network failures into “no data”, which will quietly corrupt bus-factor, network, governance, and trend views.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/github.js` around lines 88 - 101, fetchContributors and
fetchIssues currently fetch a single page (30 and 100 items) and swallow all
errors into empty arrays; update both functions (fetchContributors, fetchIssues)
to paginate the GitHub API using the per_page and page query params (or follow
the Link header) to accumulate all pages until no more results, and stop using a
blanket catch that returns []; instead propagate errors (rethrow) or return an
error result so callers can handle rate-limits/auth/network failures rather than
treating them as “no data”. Ensure you aggregate results across pages and
preserve existing use of fetchWithCache/pat while adding pagination and proper
error propagation.

Comment thread src/styles/global.css
Comment on lines +41 to +50
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-up {
animation: fadeUp 0.22s ease;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Honor prefers-reduced-motion for the new animations.

These animations run unconditionally. Add a reduced-motion override so loading and page transitions do not animate for users who explicitly disable motion.

As per coding guidelines, "Ensure that: - The code adheres to best practices recommended by lighthouse or similar tools for performance".

🧰 Tools
🪛 Stylelint (17.12.0)

[error] 44-44: Expected keyframe name "fadeUp" to be kebab-case (keyframes-name-pattern)

(keyframes-name-pattern)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/global.css` around lines 41 - 50, The animations defined by
`@keyframes` spin and fadeUp and used by .fade-up run unconditionally; add a
prefers-reduced-motion override so users who opt out of motion get no
animations: create a `@media` (prefers-reduced-motion: reduce) block that sets
animation: none (or animation-duration: 0s) and removes transform/transition
effects for .fade-up and any element relying on spin (refer to `@keyframes` spin
and the spinner class/usage), ensuring the existing keyframes stay but are not
applied when motion is reduced.

@Zahnentferner Zahnentferner merged commit 09ff28a into main May 30, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configuration Config file changes dependencies Dependency updates documentation Documentation updates first-time-contributor First time contributor frontend Frontend changes javascript JavaScript/TypeScript changes no-issue-linked PR has no linked issue size/XL 500+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants