Skip to content

feat(core): Establish Application Foundation, Dashboard UI, and API Caching Layer#53

Open
seif-a096 wants to merge 3 commits intoAOSSIE-Org:mainfrom
seif-a096:main
Open

feat(core): Establish Application Foundation, Dashboard UI, and API Caching Layer#53
seif-a096 wants to merge 3 commits intoAOSSIE-Org:mainfrom
seif-a096:main

Conversation

@seif-a096
Copy link
Copy Markdown

@seif-a096 seif-a096 commented Mar 26, 2026

Addressed Issues:

This foundational PR sets up the core infrastructure and addresses multiple open tracking issues:
Fixes #10
Fixes #15
Fixes #17
Fixes #27
Fixes #51

Screenshots/Recordings:

(Add 2-3 screenshots here of the Repositories Tab, the Overview Dashboard, and the F1 Race visualization!)

Additional Notes:

Since the repository was initially empty, this PR introduces the complete, ground-up architecture for OrgExplorer:

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 (zero TypeScript/Linting errors on npm run build)
  • 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

Summary by CodeRabbit

Release Notes

  • New Features

    • Complete dashboard overhaul with organization statistics and metrics
    • Repository browser with search, filtering, and sorting capabilities
    • Interactive contributor visualization with force-directed graph
    • GitHub Personal Access Token support for increased API access
    • Real-time API rate limit monitoring in sidebar
    • Activity timeline and language distribution charts
  • UI/Styling

    • Updated branding to "AOSSIE | WebOrg"
    • Modern design system with enhanced navigation and responsive layout
    • Animated components and improved visual feedback

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Walkthrough

This PR establishes the complete dashboard foundation for Org Explorer, implementing routing, organization data fetching with caching, GitHub API integration, and multi-page dashboard views. It introduces service layers for secure token storage and IndexedDB-backed caching, styled components with Tailwind v4, and interactive visualization features including graphs and charts.

Changes

Cohort / File(s) Summary
Project Configuration
index.html, package.json, vite.config.ts, tailwind.config.js, vercel.json, .gitignore
Updated page title/favicon, added 14 new runtime and build dependencies (Tailwind v4, routing, charting, storage), configured Tailwind/PostCSS tooling, added Vite Tailwind plugin, and set up client-side routing via Vercel rewrite rules.
Core App & Entry Points
src/main.tsx, src/App.tsx
Refactored App to use BrowserRouter with lazy-loaded routes (OverviewTab, ReposTab, ContributorsTab), added TokenModal state management, window event listeners for rate-limit handling, and layout structure with Sidebar.
Service Layer
src/services/githubApi.ts, src/services/cache.ts
Implemented githubApi object wrapping GitHub REST API calls with optional IndexedDB caching, custom error handling for rate limits, and cacheService for localStorage tokens and IndexedDB-backed data storage with 1-hour TTL.
Layout & Navigation
src/components/layout/Sidebar.tsx, src/components/common/TokenModal.tsx
Added Sidebar component with org search, API status panel, rate-limit display, token connection, and manual refresh; added TokenModal for secure GitHub PAT entry with localStorage persistence and page reload on save.
Dashboard Pages
src/components/dashboard/OverviewTab.tsx, src/components/dashboard/ReposTab.tsx, src/components/dashboard/ContributorsTab.tsx
Implemented three lazy-loaded dashboard pages: OverviewTab (~595 lines) with aggregated stats, language charts, activity timeline, and contributor leaderboards; ReposTab (~439 lines) with searchable/filterable/sortable repo table and detail panel; ContributorsTab (~238 lines) with force-directed graph visualization of contributors and repositories.
UI Components & Charts
src/components/dashboard/StatCard.tsx, src/components/dashboard/LanguageChart.tsx, src/components/dashboard/RepoDetailPanel.tsx, src/components/dashboard/ContributorRepoGraph.tsx
Added animated stat cards, pie chart for language distribution, sliding detail panel for repo stats/contributors, and interactive force-directed graph with custom canvas rendering, hover state management, and legend.
Styling & Utilities
src/index.css, src/lib/utils.ts, src/hooks/useRateLimit.ts
Replaced default CSS with Tailwind v4 theme integration, Google Fonts imports, custom theme variables (GitHub-inspired palette), and CSS utilities; added cn utility for Tailwind class merging; introduced useRateLimit hook for polling and event-driven rate-limit tracking.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as UI (TokenModal)
    participant Cache as cacheService
    participant Storage as localStorage
    participant Browser as window.location
    
    User->>UI: Opens app without token
    UI->>Cache: Calls cacheService.getToken()
    Cache->>Storage: Checks localStorage for token
    Storage-->>Cache: Returns null
    Cache-->>UI: No token found
    UI->>UI: Shows TokenModal
    User->>UI: Enters PAT and clicks "Save & Continue"
    UI->>UI: Validates token (trim, not empty)
    UI->>Cache: Calls cacheService.setToken(token)
    Cache->>Storage: Stores token in localStorage
    Storage-->>Cache: Stored
    Cache-->>UI: Callback complete
    UI->>Browser: Calls window.location.reload()
    Browser->>Browser: Page reloads
    Note over Browser: App reinitializes with stored token
Loading
sequenceDiagram
    participant App as React Component
    participant GithubAPI as githubApi
    participant IndexedDB as cacheService (IndexedDB)
    participant GitHub as GitHub API
    
    App->>GithubAPI: Calls getOrgRepos(orgName)
    GithubAPI->>IndexedDB: Calls get("repos", cacheKey)
    alt Cache Hit (< 1 hour)
        IndexedDB-->>GithubAPI: Returns cached repos
    else Cache Miss or Stale
        GithubAPI->>GitHub: Fetches /repos endpoint
        GitHub-->>GithubAPI: Returns repo data
        GithubAPI->>IndexedDB: Calls set("repos", cacheKey, data)
        IndexedDB->>IndexedDB: Stores in repos store + metadata timestamp
        IndexedDB-->>GithubAPI: Write complete
    end
    GithubAPI-->>App: Returns repos array
    App->>App: Renders dashboard with data
    Note over App: Re-renders use cached data when available<br/>Rate limit preserved via caching layer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #2: Establishes initial project bootstrap with HTML title and package.json structure that this PR extends with new dependencies and application logic.
  • PR #7: Introduces Tailwind CSS v4 configuration, the cn utility, and theming setup that directly underlies the styling used throughout this PR's components.

Suggested labels

Typescript Lang

Suggested reviewers

  • Zahnentferner

Poem

🐰 A dashboard springs to life with careful care,
GitHub orgs now sparkle everywhere,
Cache and tokens safely stored,
Force graphs dance, data adored,
From tokens to charts—explore the air!

🚥 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 accurately summarizes the main change: establishing the application foundation, dashboard UI, and API caching layer, which aligns with the primary objectives of setting up core architecture and services.
Linked Issues check ✅ Passed The PR comprehensively addresses all linked issues: #10 (Tailwind CSS v4 migration with @tailwindcss/vite), #15 (basic dashboard with OverviewTab/ReposTab/ContributorsTab), #17 (UI foundation with Tailwind/Recharts/D3), #27 (localStorage PAT storage and IndexedDB caching), and #51 (dashboard analytics with org stats and charts).
Out of Scope Changes check ✅ Passed All changes are directly aligned with establishing the application foundation. The PR includes routing, API caching, dashboard components, styling/theming, and supporting infrastructure—all core to the stated objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Establishes the initial OrgExplorer frontend foundation: Vite + React Router app shell, Tailwind v4 styling/theme tokens, core dashboard tabs (overview/repos/contributors) with visualizations, and a client-side GitHub API wrapper backed by IndexedDB caching.

Changes:

  • Adds Tailwind v4 + shared UI utilities and baseline styling/theme setup.
  • Introduces dashboard layout, routing, and core tabs with charts/visualizations.
  • Implements a GitHub API + IndexedDB cache layer and PAT token modal flow.

Reviewed changes

Copilot reviewed 20 out of 23 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
vite.config.ts Adds Tailwind Vite plugin alongside React plugin.
vercel.json Adds SPA rewrite rules for static hosting.
tailwind.config.js Tailwind content + theme color extensions.
src/services/githubApi.ts GitHub API wrapper with cache-first fetch and rate-limit signaling.
src/services/cache.ts IndexedDB cache + localStorage persistence helpers (token/org).
src/main.tsx Updates React bootstrap entrypoint formatting/imports.
src/lib/utils.ts Adds cn() helper using clsx + tailwind-merge.
src/index.css Tailwind v4 CSS-first theme tokens + base styling + utilities.
src/hooks/useRateLimit.ts Hook to poll/display GitHub rate limit information.
src/components/layout/Sidebar.tsx Adds sidebar nav + API status widget + token CTA.
src/components/dashboard/StatCard.tsx Adds animated stat card component.
src/components/dashboard/ReposTab.tsx Repos table with search/sort/pagination + detail panel trigger.
src/components/dashboard/RepoDetailPanel.tsx Slide-over repo details, activity chart, top contributors.
src/components/dashboard/OverviewTab.tsx Overview dashboard: org header, stat cards, language chart, F1 contributors, top repos.
src/components/dashboard/LanguageChart.tsx Recharts pie chart for top languages.
src/components/dashboard/F1Race.tsx framer-motion “race” visualization for contributors.
src/components/dashboard/ContributorsTab.tsx Contributors grid aggregated from active repos.
src/components/common/TokenModal.tsx Modal for entering/storing GitHub PAT.
src/App.tsx App shell with router + lazy routes + token modal integration.
public/aossie-icon.svg Adds AOSSIE-branded SVG icon.
package.json Adds UI/chart/router/idb deps and Tailwind tooling.
package-lock.json Lockfile updates for added dependencies.
index.html Adds favicon link and updates document title.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +31
const data = await response.json();
return {
limit: data.rate.limit,
remaining: data.rate.remaining,
reset: data.rate.reset,
};
} catch {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

getRateLimit() parses JSON and returns data.rate.* without checking response.ok. On 401/403/5xx, GitHub returns a different payload shape, which will throw and get swallowed as null, and it also won’t dispatch the github-api-limit event like fetchWithCache does. Add an ok/status check (and optionally dispatch the same event for 401/403) before reading data.rate so consumers can distinguish unauthenticated/rate-limited vs network errors.

Suggested change
const data = await response.json();
return {
limit: data.rate.limit,
remaining: data.rate.remaining,
reset: data.rate.reset,
};
} catch {
if (!response.ok) {
if (response.status === 403 || response.status === 401) {
// Trigger generic rate limit / auth event, consistent with fetchWithCache
window.dispatchEvent(new CustomEvent('github-api-limit', { detail: response.headers }));
}
throw new GitHubApiError(response.status, `GitHub API Error: ${response.statusText}`);
}
const data = await response.json();
return {
limit: data.rate.limit,
remaining: data.rate.remaining,
reset: data.rate.reset,
};
} catch (error) {
// Propagate API errors so callers can distinguish them from network/parse failures
if (error instanceof GitHubApiError) {
throw error;
}

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +60
const response = await fetch(`${API_BASE}${url}`, {
headers: getHeaders(),
});

if (!response.ok) {
if (response.status === 403 || response.status === 401) {
// Trigger generic rate limit event
window.dispatchEvent(new CustomEvent('github-api-limit', { detail: response.headers }));
}
throw new GitHubApiError(response.status, `GitHub API Error: ${response.statusText}`);
}

const data = await response.json();
await cacheService.set(storeName, cacheKey, data);
return data;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

fetchWithCache() will cache any response.ok payload, including GitHub’s 202 Accepted responses from /stats/* endpoints (which typically return a “processing” message). Caching that transient payload can permanently turn the activity view into an empty state until TTL expires. Handle response.status === 202 (and possibly 204) as a cache miss: don’t write to IndexedDB and either return an empty array/typed placeholder or retry with backoff.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +34
const fetchRateLimit = async () => {
setIsLoading(true);
const limitInfo = await githubApi.getRateLimit();
setRateLimit(limitInfo);
setIsLoading(false);
};

useEffect(() => {
fetchRateLimit();

const handleLimitHit = () => {
fetchRateLimit();
};

window.addEventListener('github-api-limit', handleLimitHit);

// Poll every 5 minutes or on tab focus
const interval = setInterval(fetchRateLimit, 5 * 60 * 1000);
window.addEventListener('focus', fetchRateLimit);

return () => {
window.removeEventListener('github-api-limit', handleLimitHit);
window.removeEventListener('focus', fetchRateLimit);
clearInterval(interval);
};
}, []);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

fetchRateLimit is referenced inside a useEffect with an empty dependency array. With eslint-plugin-react-hooks enabled, this will trigger react-hooks/exhaustive-deps warnings and can also lead to stale closures if the function later captures values. Wrap fetchRateLimit in useCallback and include it (and any other referenced values) in the effect dependency list.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +18
if (token.trim()) {
cacheService.setToken(token.trim());
window.location.reload(); // Reload to re-fetch with token
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

handleSave() forces a full page reload after saving the token. This breaks SPA flow and can reset other client state unnecessarily. Prefer closing the modal and triggering refetches via state/context (or dispatching a dedicated event) so the app updates without a hard reload.

Suggested change
if (token.trim()) {
cacheService.setToken(token.trim());
window.location.reload(); // Reload to re-fetch with token
const trimmedToken = token.trim();
if (trimmedToken) {
cacheService.setToken(trimmedToken);
// Notify the application that the token has been updated without forcing a full reload
window.dispatchEvent(new CustomEvent('tokenUpdated'));
onClose();

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +205
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-1.5 sharp-interactive bg-github-dark disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-1.5 sharp-interactive bg-github-dark disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="w-5 h-5" />
</button>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

Pagination buttons are icon-only (chevrons) and don’t have an accessible label, so screen-reader users won’t know what they do. Add aria-label (e.g., “Previous page” / “Next page”) and consider reflecting the disabled state via aria-disabled as well.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +97
<div className="w-full bg-github-border h-1 flex">
<div
className={cn(
"h-full transition-all duration-300",
rateLimit && (rateLimit.remaining / rateLimit.limit) > 0.2 ? "bg-[var(--color-aossie-green)]" : "bg-red-500"
)}
style={{
width: rateLimit ? `${(rateLimit.remaining / rateLimit.limit) * 100}%` : '0%'
}}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The rate-limit progress calculations divide by rateLimit.limit without guarding against 0/missing values. If getRateLimit() returns an unexpected payload (or limit is 0), this yields NaN%/Infinity% widths and incorrect coloring. Clamp the ratio with a safe denominator (e.g., Math.max(limit, 1)) and consider an explicit isLoading || !rateLimit branch.

Copilot uses AI. Check for mistakes.
</div>
<button
onClick={onClose}
className="text-github-muted hover:text-white transition-colors p-1"
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The modal close button is icon-only (X) and lacks an accessible name. Add an aria-label (e.g., “Close token dialog”) so screen readers can announce it correctly.

Suggested change
className="text-github-muted hover:text-white transition-colors p-1"
className="text-github-muted hover:text-white transition-colors p-1"
aria-label="Close token dialog"

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +71
<button
onClick={onClose}
className="p-2 sharp-interactive text-github-muted hover:text-white transition-colors ml-4 bg-github-dark"
>
<X className="w-5 h-5" />
</button>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The repo detail panel close button is icon-only and doesn’t provide an accessible name. Add an aria-label (and optionally type="button") so it’s properly announced and doesn’t accidentally submit if nested in a form in the future.

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 40
"dependencies": {
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
"idb": "^8.0.3",
"lucide-react": "^1.7.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.2",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.1.18",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "npm:rolldown-vite@7.2.5"
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

No Node.js engine requirement is declared, but dependencies introduced/updated here require Node >= 20 (notably react-router-dom@7 and rolldown-vite). Without an engines.node (and/or an .nvmrc), installs/builds may fail on CI or contributor machines running Node 18. Add an explicit Node version range that matches the strictest dependency requirement.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +7
<link rel="icon" type="image/svg+xml" href="/aossie-icon.svg" />
<title>AOSSIE | WebOrg</title>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The document title was changed to “AOSSIE | WebOrg”, but the PR/issue context and app naming elsewhere refer to “OrgExplorer”. If this isn’t an intentional rename, align the <title> with the project name (and any expected branding) to avoid confusing users and affecting browser history/search results.

Copilot uses AI. Check for mistakes.
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: 28

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/App.tsx`:
- Line 27: The Suspense fallback in App.tsx currently contains the hardcoded
user-visible string "LOADING_MODULE..."; update it to use your i18n resource
instead: add/import the translation hook or helper (e.g., useTranslation/t from
your i18n setup) in the App component, add a key like "loadingModule" to the
i18n resource files, and replace the inline text with the translated value
(e.g., t('loadingModule') or <Trans i18nKey="loadingModule" />) while keeping
the existing fallback element and its classes intact so styling is unchanged.
- Around line 28-33: The Routes block in App.tsx lacks a fallback for unmatched
URLs, causing blank screens; add a catch-all Route (path="*") at the end of the
<Routes> list that renders a sensible fallback (e.g., Navigate to "/overview" or
a NotFound component) so unknown paths are handled; update the existing <Routes>
where Route components (Route path="/" element={<Navigate .../>}, Route
path="/overview" element={<OverviewTab/>}, etc.) are declared and append the new
Route with path="*" to prevent empty renders.

In `@src/components/common/TokenModal.tsx`:
- Around line 28-80: The TokenModal component currently contains hard-coded
user-visible strings; extract all text (titles, descriptions, labels,
placeholders, button text, and link text) into i18n resource keys and replace
literals in TokenModal (e.g., the "Connect GitHub Token" header, the paragraph
about token usage, "Personal Access Token" label, placeholder "ghp_...", the
scope description and link text, "Cancel", and "Save & Continue") with localized
lookups (e.g., t('tokenModal.title')) and ensure TokenModal imports/uses the
i18n hook/function (e.g., useTranslation or t) so token, handleSave, and onClose
logic remain unchanged while strings come from resource files.
- Around line 15-19: The current handleSave handler stores the GitHub PAT via
cacheService.setToken(token.trim()) and forces a reload, which persists the
token in client-accessible storage; change this to send the trimmed token to the
backend over HTTPS and let the server set a secure, HTTP-only cookie instead of
calling cacheService.setToken or writing to localStorage. Update handleSave to
POST the token to an auth endpoint (e.g., /api/auth/token) including CSRF
protection and handle the server response (success -> window.location.reload()
if needed, failure -> show error), remove direct local persistence calls to
cacheService.setToken, and ensure token state (the token variable) is cleared
after submission; coordinate with backend to validate and set the secure cookie.
- Around line 23-53: Add proper dialog semantics to the TokenModal component:
give the modal container the attributes role="dialog" and aria-modal="true" and
set aria-labelledby to the heading's id (add an id to the <h2> used in the
header, e.g., modal-title). Ensure the label/input are associated by converting
the visible label to a <label htmlFor="token-input"> and give the input an
id="token-input" (keep value={token} and onChange calling setToken). Add
aria-describedby on the dialog pointing to the explanatory paragraph's id for
additional context, and add an accessible name to the close button (e.g.,
aria-label="Close token modal" on the button that calls onClose).

In `@src/components/dashboard/ContributorsTab.tsx`:
- Line 7: The contributors state currently uses the any[] type; define an
explicit interface (e.g., Contributor with fields login, avatar_url, html_url,
contributions) and replace useState<any[]>() with useState<Contributor[]>() and
update any related variables/params/usages (e.g., setContributors, any local
variables or map callbacks that expect contributor objects) to use the
Contributor type so all occurrences (including uses in render/mapping) have
explicit typing instead of any.
- Line 10: cacheService.getLastOrg() can return null/undefined so guard the
value before using orgName in the downstream API call: after const orgName =
cacheService.getLastOrg(), check if (!orgName) and handle it (early return, show
a placeholder/empty state, or log and skip the API request) instead of passing
undefined into the API; update the code paths that call the API with orgName
(the API call that currently uses orgName) to only run when orgName is truthy or
to use a safe default, and ensure UI state is updated to reflect the missing
org.

In `@src/components/dashboard/F1Race.tsx`:
- Around line 77-80: The avatar <img> in F1Race.tsx currently uses
src={racer.avatar_url} and has no error handling, so add an onError handler to
replace the broken image with a fallback (for example a local /static
placeholder or data-URI) and ensure the handler uses the image element
(e.currentTarget.src = fallback) and idempotently avoids infinite loops (check
current src before replacing or use a boolean state like hasAvatarError). Update
the <img> element in the Avatar (Driver) block to call the new onError and
optionally use a small piece of component state (e.g., hasAvatarError) or guard
to pick between racer.avatar_url and the fallback.

In `@src/components/dashboard/LanguageChart.tsx`:
- Line 44: In LanguageChart update the tooltip/axis formatter signature to
replace the any type with an explicit numeric type: change the formatter param
on the formatter={(value: any) => ...} callback to use (value: number) (or
number | null if nulls are possible) so it matches dataKey="value"; ensure any
string interpolation still works by converting the number to string where needed
and keep the returned array structure unchanged.

In `@src/components/dashboard/OverviewTab.tsx`:
- Around line 24-29: State uses untyped any/any[] for languages, racers,
topRepos, and orgData which weakens TypeScript safety; define explicit
interfaces (e.g., Language, Racer, Repo, OrgData) and replace
useState<any[]>/useState<any> with useState<Language[]>, useState<Racer[]>,
useState<Repo[]>, and useState<OrgData | null> respectively in OverviewTab.tsx,
update setLanguages, setRacers, setTopRepos, and setOrgData usages to conform to
those shapes, and import or declare the interfaces near the top of the file so
the types are enforced across functions that read/write these states.
- Around line 97-99: The component OverviewTab currently only logs fetch
failures; add an error state (e.g., const [error, setError] = useState<string |
null>(null)) and in the catch block of the data-loading function setError with a
user-facing message (or the error.message), clear it on successful loads, and
expose a retry handler that re-invokes the fetch; then update OverviewTab's
render to show conditional fallback UI when error is non-null (toast, inline
message, and/or a Retry button that calls the same fetch function) so users see
and can recover from load failures.
- Around line 81-89: Replace the implicit any with a proper Contributor type and
use Record<string, Contributor> for combinedContributors; specifically define an
interface (e.g., Contributor with login: string, contributions: number, plus
optional fields) and change combinedContributors from Record<string, any> to
Record<string, Contributor>, then update the aggregation loop (the
contributorsArrays.flat().forEach block that references c.login and
c.contributions) to operate against that typed shape and keep the null/undefined
guard (if (!c || !c.login) return) so property accesses are type-safe.

In `@src/components/dashboard/RepoDetailPanel.tsx`:
- Line 64: Replace hardcoded user-visible strings in RepoDetailPanel with i18n
resource keys: remove literal 'NO DESCRIPTION' used next to repo.description,
the "No activity data available." message, and "NO CONTRIBUTORS DATA." and
instead call the localization helper (e.g., t('repo.noDescription'),
t('repo.noActivity'), t('repo.noContributors')) or import strings from the app's
resource bundle; update the JSX in RepoDetailPanel to use these keys
(references: the repo.description render, the activity fallback at lines
~124-126, and the contributors fallback at ~171-173), add the corresponding
entries to the locale resource file(s), and import/initialize the translator in
RepoDetailPanel so all three UI strings come from the i18n resources.
- Around line 7-15: Replace the broad any types by defining explicit interfaces
(e.g. Repo, Contributor, Activity) and use them in RepoDetailPanelProps and the
component state: change repo: any | null to repo: Repo | null in
RepoDetailPanelProps, change contributors state to useState<Contributor[]>([])
and activity state to useState<Activity[]>([]) (and update any handlers that
call setContributors/setActivity to accept these types); ensure the interfaces
include the fields the component reads (e.g. name, owner, stars for Repo; login,
avatarUrl for Contributor; date, type, description for Activity) so all property
accesses inside RepoDetailPanel are properly typed.
- Around line 17-49: The useEffect closure reads orgName from
cacheService.getLastOrg() but the effect dependency array only lists repo, so
the effect won't re-run when orgName changes; update the dependency array to
include orgName (or derive orgName outside/useMemo) so that fetchData (which
calls githubApi.getRepoContributors and githubApi.getRepoActivity) re-executes
with the current orgName; ensure useEffect([...]) includes orgName alongside
repo to fix stale org fetches.

In `@src/components/dashboard/ReposTab.tsx`:
- Line 20: cacheService.getLastOrg() can return null/undefined so add a guard in
the ReposTab component before using orgName: check the result of
cacheService.getLastOrg() (the orgName variable) and bail out or return early
(e.g., no-op render or skip API call) when it's falsy, and only call the repos
API or invoke the function that uses orgName (the code around the orgName
assignment and the subsequent call at the second occurrence) when orgName is a
non-empty string; update both places that reference orgName (the initial const
orgName = cacheService.getLastOrg() and the later use) to perform this guard to
prevent passing undefined/null into the API call.
- Line 170: The date rendering in ReposTab currently uses new
Date(repo.updated_at).toLocaleDateString(), which varies by browser locale;
update the JSX to use date-fns for a consistent format: import format and
parseISO from 'date-fns' at the top of the ReposTab component file and replace
new Date(repo.updated_at).toLocaleDateString() with
format(parseISO(repo.updated_at), 'yyyy-MM-dd') (or your preferred format
string) to ensure deterministic output and handle ISO strings safely.
- Around line 11-14: Replace the broad any types by defining an explicit
Repository interface (e.g., fields like id, name, description?, language?,
stargazers_count, forks_count, open_issues_count, updated_at, etc.) at the top
of ReposTab.tsx, then change the state generics from useState<any[]>([]) to
useState<Repository[]>([]) for repos and from useState<any | null>(null) to
useState<Repository | null>(null) for selectedRepo; also update any handlers or
render code that access repository properties (e.g., where repos, setRepos,
selectedRepo are used) to rely on the new typed fields or make optional checks
where fields may be null/undefined.

In `@src/components/layout/Sidebar.tsx`:
- Around line 93-96: The current expressions in Sidebar.tsx use
(rateLimit.remaining / rateLimit.limit) without guarding against rateLimit.limit
=== 0; update both the className ternary and the style width calculation to
first verify rateLimit && rateLimit.limit > 0 (or compute a safeRatio =
rateLimit && rateLimit.limit > 0 ? rateLimit.remaining / rateLimit.limit : 0)
and then use safeRatio for the > 0.2 check and for the width `${safeRatio *
100}%` so you never divide by zero and invalid CSS is avoided.

In `@src/hooks/useRateLimit.ts`:
- Around line 23-27: The focus handler and interval in useRateLimit are causing
overlapping fetchRateLimit calls; wrap fetchRateLimit in a debounced or
cancellable wrapper and register that instead of calling fetchRateLimit
directly. Specifically, create a debouncedFetchRateLimit using useCallback (or
implement an AbortController inside a useCallback that cancels prior fetches)
and replace window.addEventListener('focus', fetchRateLimit) and the interval
callback with this wrapper; ensure you clear the interval, remove both event
listeners, and abort any in-flight request in the hook cleanup to avoid
redundant API calls and race conditions.
- Around line 9-14: The fetchRateLimit function can throw from
githubApi.getRateLimit causing setIsLoading(true) to never be cleared; wrap the
await call in a try/catch/finally (or try/finally) inside fetchRateLimit, call
setRateLimit only on success, handle/log the error in the catch, and ensure
setIsLoading(false) is executed in the finally block so the loading state is
always cleared.

In `@src/index.css`:
- Around line 28-41: The current CSS only defines WebKit pseudo-elements (::
-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover) which only affect Chromium browsers; add
cross‑browser rules using the standard properties scrollbar-color and
scrollbar-width on the scrollable elements (or :root) and set sensible values
that map to your existing variables (e.g., thumb and track colors and width) so
Firefox and other non‑WebKit browsers show a matching styled scrollbar; keep the
WebKit rules as-is and add the standard properties alongside them to ensure
broad support.

In `@src/services/cache.ts`:
- Around line 52-78: The get and set IndexedDB operations (functions get and set
using getDB, TTL_MS, db.transaction, tx.objectStore) lack error handling and can
cause unhandled rejections; wrap the body of both get and set in try/catch
blocks, catch any errors from await getDB(), db.get(), and the transaction
Promise, log the error with contextual info (storeName and key) and for get
return null on failure, while for set log the error and ensure the transaction
is aborted/cleaned up (call tx.abort if available) without letting the exception
bubble up to callers.
- Around line 4-23: OrgExplorerDB currently types every store value as any which
loses TypeScript safety; replace those any types with explicit interfaces or
generics (e.g., define Repo, Contributor, Activity, Metadata interfaces/types)
and use them in the OrgExplorerDB stores (repos, contributors, activity,
metadata.value) or add generic type parameters to your cache accessors (get<T>,
set<T>) so callers can specify the expected type; update references to
OrgExplorerDB, repos, contributors, activity, metadata and the cache get/set
methods to use the new types.

In `@src/services/githubApi.ts`:
- Around line 20-34: In getRateLimit, check response.ok before calling
response.json() and accessing data.rate: if !response.ok return null (or handle
the error) to avoid accessing undefined data.rate; update the getRateLimit
function to first test response.ok on the fetch response, only parse JSON and
read data.rate.limit / data.rate.remaining / data.rate.reset when response.ok is
true, otherwise return null (or propagate an error) so you don't attempt to read
properties from an undefined rate object.
- Around line 72-97: Replace the generic any[] return types by defining precise
response interfaces (e.g., OrgRepo, RepoContributor, CommitActivity) that match
the GitHub API shapes and use those interfaces as the generic type parameters on
githubApi.fetchWithCache and the functions getOrgRepos, getRepoContributors, and
getRepoActivity so each function returns Promise<OrgRepo[]>,
Promise<RepoContributor[]>, and Promise<CommitActivity[]> respectively; add or
import those interfaces near this module and update the function signatures and
fetchWithCache calls to use them for proper type safety.
- Around line 46-56: Add a request timeout to the GitHub API fetch in
src/services/githubApi.ts by using an AbortController: create an AbortController
before calling fetch, pass controller.signal to fetch (alongside headers from
getHeaders()), and set a timer (e.g., via setTimeout) that calls
controller.abort() after the desired timeout; clear the timer after fetch
completes. Catch the abort case (check for DOMException name 'AbortError' or
similar) and rethrow a GitHubApiError with a clear timeout message so callers
can handle it, while preserving the existing status-based logic (including the
github-api-limit event dispatch) for non-timeout failures. Ensure the timer is
cleaned up in all code paths.
- Around line 63-70: getOrgDetails currently calls the GitHub API without
validating the org parameter; add input validation at the start of getOrgDetails
to ensure org is a non-empty string matching GitHub org name rules (e.g. allowed
chars and length, such as /^[A-Za-z0-9-]{1,39}$/), and throw a clear error (e.g.
"Invalid organization name") before making the fetch; apply the same validation
pattern to other service methods that accept org or repo parameters to prevent
malformed API calls.
🪄 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: fef4ce7f-2c19-4c2c-a5b5-1a2de5321151

📥 Commits

Reviewing files that changed from the base of the PR and between 1cfc3e7 and 2b55919.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • public/aossie-icon.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • index.html
  • package.json
  • src/App.tsx
  • src/components/common/TokenModal.tsx
  • src/components/dashboard/ContributorsTab.tsx
  • src/components/dashboard/F1Race.tsx
  • src/components/dashboard/LanguageChart.tsx
  • src/components/dashboard/OverviewTab.tsx
  • src/components/dashboard/RepoDetailPanel.tsx
  • src/components/dashboard/ReposTab.tsx
  • src/components/dashboard/StatCard.tsx
  • src/components/layout/Sidebar.tsx
  • src/hooks/useRateLimit.ts
  • src/index.css
  • src/lib/utils.ts
  • src/main.tsx
  • src/services/cache.ts
  • src/services/githubApi.ts
  • tailwind.config.js
  • vercel.json
  • vite.config.ts

<Sidebar className="w-64 border-r border-github-border flex-shrink-0" onConnectToken={() => setShowTokenModal(true)} />

<main className="flex-1 overflow-y-auto p-8 relative">
<Suspense fallback={<div className="flex w-full h-full items-center justify-center text-aossie-green font-mono">LOADING_MODULE...</div>}>
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

Externalize the loading fallback string.

LOADING_MODULE... is user-visible and should come from i18n resources, not inline text.

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

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` at line 27, The Suspense fallback in App.tsx currently contains
the hardcoded user-visible string "LOADING_MODULE..."; update it to use your
i18n resource instead: add/import the translation hook or helper (e.g.,
useTranslation/t from your i18n setup) in the App component, add a key like
"loadingModule" to the i18n resource files, and replace the inline text with the
translated value (e.g., t('loadingModule') or <Trans i18nKey="loadingModule" />)
while keeping the existing fallback element and its classes intact so styling is
unchanged.

Comment on lines +28 to +33
<Routes>
<Route path="/" element={<Navigate to="/overview" replace />} />
<Route path="/overview" element={<OverviewTab />} />
<Route path="/repos" element={<ReposTab />} />
<Route path="/contributors" element={<ContributorsTab />} />
</Routes>
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether a wildcard route exists in the app routing table.
rg -nP --type=tsx '<Route\s+path="\*"' src/App.tsx

Repository: AOSSIE-Org/OrgExplorer

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Check if src/App.tsx exists and read the relevant lines
if [ -f "src/App.tsx" ]; then
  echo "=== File exists ==="
  wc -l src/App.tsx
  echo ""
  echo "=== Lines 20-40 (context around lines 28-33) ==="
  sed -n '20,40p' src/App.tsx
  echo ""
  echo "=== Search for wildcard routes in src/App.tsx ==="
  rg 'path="\*"' src/App.tsx -n
else
  echo "src/App.tsx not found"
  find . -name "App.tsx" -o -name "app.tsx" 2>/dev/null | head -5
fi

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1156


Add a catch-all route to prevent blank screens on unknown URLs.

Unmatched paths currently render no content. Add a fallback path="*" route.

Proposed fix
            <Routes>
              <Route path="/" element={<Navigate to="/overview" replace />} />
              <Route path="/overview" element={<OverviewTab />} />
              <Route path="/repos" element={<ReposTab />} />
              <Route path="/contributors" element={<ContributorsTab />} />
+             <Route path="*" element={<Navigate to="/overview" replace />} />
            </Routes>
📝 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
<Routes>
<Route path="/" element={<Navigate to="/overview" replace />} />
<Route path="/overview" element={<OverviewTab />} />
<Route path="/repos" element={<ReposTab />} />
<Route path="/contributors" element={<ContributorsTab />} />
</Routes>
<Routes>
<Route path="/" element={<Navigate to="/overview" replace />} />
<Route path="/overview" element={<OverviewTab />} />
<Route path="/repos" element={<ReposTab />} />
<Route path="/contributors" element={<ContributorsTab />} />
<Route path="*" element={<Navigate to="/overview" replace />} />
</Routes>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 28 - 33, The Routes block in App.tsx lacks a
fallback for unmatched URLs, causing blank screens; add a catch-all Route
(path="*") at the end of the <Routes> list that renders a sensible fallback
(e.g., Navigate to "/overview" or a NotFound component) so unknown paths are
handled; update the existing <Routes> where Route components (Route path="/"
element={<Navigate .../>}, Route path="/overview" element={<OverviewTab/>},
etc.) are declared and append the new Route with path="*" to prevent empty
renders.

Comment on lines +15 to +19
const handleSave = () => {
if (token.trim()) {
cacheService.setToken(token.trim());
window.location.reload(); // Reload to re-fetch with token
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify token storage backend used by cacheService.setToken.
fd 'cache.*' src
rg -nP -C3 --glob 'src/**/*.{ts,tsx,js,jsx}' '\bsetToken\s*\(|localStorage|sessionStorage|indexedDB'

Repository: AOSSIE-Org/OrgExplorer

Length of output: 2007


Remove localStorage usage for token persistence; use secure HTTP-only cookies instead.

GitHub PAT tokens are stored in localStorage (via cacheService.setToken() at line 17), which persists across sessions and is vulnerable to XSS attacks. Store sensitive authentication credentials in secure HTTP-only cookies that the server can validate and rotate, preventing client-side exposure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/TokenModal.tsx` around lines 15 - 19, The current
handleSave handler stores the GitHub PAT via cacheService.setToken(token.trim())
and forces a reload, which persists the token in client-accessible storage;
change this to send the trimmed token to the backend over HTTPS and let the
server set a secure, HTTP-only cookie instead of calling cacheService.setToken
or writing to localStorage. Update handleSave to POST the token to an auth
endpoint (e.g., /api/auth/token) including CSRF protection and handle the server
response (success -> window.location.reload() if needed, failure -> show error),
remove direct local persistence calls to cacheService.setToken, and ensure token
state (the token variable) is cleared after submission; coordinate with backend
to validate and set the secure cookie.

Comment on lines +23 to +53
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200">
<div className="flex items-center justify-between p-4 border-b border-github-border">
<div className="flex items-center gap-2 text-white">
<Key className="w-5 h-5" />
<h2 className="font-semibold text-lg">Connect GitHub Token</h2>
</div>
<button
onClick={onClose}
className="text-github-muted hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>

<div className="p-6 space-y-4">
<p className="text-sm text-github-text">
Required to continue browsing or to access private repositories. Your token never leaves your browser.
</p>

<div className="space-y-2">
<label className="text-xs font-semibold text-white uppercase tracking-wider">
Personal Access Token
</label>
<input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="ghp_..."
className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono"
/>
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

Modal is missing key accessibility semantics.

Add dialog attributes and accessible control labeling (role="dialog", aria-modal, aria-labelledby, close button aria-label, and label/input association).

Proposed fix
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
-      <div className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="token-modal-title"
+        className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200"
+      >
@@
-            <h2 className="font-semibold text-lg">Connect GitHub Token</h2>
+            <h2 id="token-modal-title" className="font-semibold text-lg">Connect GitHub Token</h2>
@@
-          <button 
+          <button
             onClick={onClose}
+            aria-label="Close token modal"
             className="text-github-muted hover:text-white transition-colors p-1"
           >
@@
-            <label className="text-xs font-semibold text-white uppercase tracking-wider">
+            <label htmlFor="github-token" className="text-xs font-semibold text-white uppercase tracking-wider">
               Personal Access Token
             </label>
             <input
+              id="github-token"
               type="password"

As per coding guidelines, the code should follow Lighthouse-aligned best practices for task completion and usability.

📝 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
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200">
<div className="flex items-center justify-between p-4 border-b border-github-border">
<div className="flex items-center gap-2 text-white">
<Key className="w-5 h-5" />
<h2 className="font-semibold text-lg">Connect GitHub Token</h2>
</div>
<button
onClick={onClose}
className="text-github-muted hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4">
<p className="text-sm text-github-text">
Required to continue browsing or to access private repositories. Your token never leaves your browser.
</p>
<div className="space-y-2">
<label className="text-xs font-semibold text-white uppercase tracking-wider">
Personal Access Token
</label>
<input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="ghp_..."
className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono"
/>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div
role="dialog"
aria-modal="true"
aria-labelledby="token-modal-title"
className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200"
>
<div className="flex items-center justify-between p-4 border-b border-github-border">
<div className="flex items-center gap-2 text-white">
<Key className="w-5 h-5" />
<h2 id="token-modal-title" className="font-semibold text-lg">Connect GitHub Token</h2>
</div>
<button
onClick={onClose}
aria-label="Close token modal"
className="text-github-muted hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4">
<p className="text-sm text-github-text">
Required to continue browsing or to access private repositories. Your token never leaves your browser.
</p>
<div className="space-y-2">
<label htmlFor="github-token" className="text-xs font-semibold text-white uppercase tracking-wider">
Personal Access Token
</label>
<input
id="github-token"
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="ghp_..."
className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/TokenModal.tsx` around lines 23 - 53, Add proper dialog
semantics to the TokenModal component: give the modal container the attributes
role="dialog" and aria-modal="true" and set aria-labelledby to the heading's id
(add an id to the <h2> used in the header, e.g., modal-title). Ensure the
label/input are associated by converting the visible label to a <label
htmlFor="token-input"> and give the input an id="token-input" (keep
value={token} and onChange calling setToken). Add aria-describedby on the dialog
pointing to the explanatory paragraph's id for additional context, and add an
accessible name to the close button (e.g., aria-label="Close token modal" on the
button that calls onClose).

Comment on lines +28 to +80
<h2 className="font-semibold text-lg">Connect GitHub Token</h2>
</div>
<button
onClick={onClose}
className="text-github-muted hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>

<div className="p-6 space-y-4">
<p className="text-sm text-github-text">
Required to continue browsing or to access private repositories. Your token never leaves your browser.
</p>

<div className="space-y-2">
<label className="text-xs font-semibold text-white uppercase tracking-wider">
Personal Access Token
</label>
<input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="ghp_..."
className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono"
/>
</div>

<div className="flex items-start gap-2 text-xs text-github-muted bg-github-canvas p-3 sharp-border border-dashed">
<ShieldCheck className="w-4 h-4 shrink-0 mt-0.5 text-[var(--color-aossie-yellow)]" />
<p className="font-mono">
We only request read:org and public_repo scopes. Store token in
<a href="https://github.com/settings/tokens" target="_blank" rel="noreferrer" className="text-[var(--color-aossie-green)] hover:underline mx-1 font-bold">
GitHub Settings
</a>.
</p>
</div>
</div>

<div className="p-4 border-t border-github-border bg-github-dark flex justify-end gap-3 font-mono">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-github-muted uppercase font-bold hover:text-white transition-colors sharp-interactive border-transparent"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={!token.trim()}
className="px-4 py-2 text-sm bg-[var(--color-aossie-green)] hover:bg-[var(--color-aossie-yellow)] text-black sharp-border font-black uppercase transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Save & Continue
</button>
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

Externalize modal copy for localization.

All user-facing strings in this modal should be moved to i18n resource files.

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

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/TokenModal.tsx` around lines 28 - 80, The TokenModal
component currently contains hard-coded user-visible strings; extract all text
(titles, descriptions, labels, placeholders, button text, and link text) into
i18n resource keys and replace literals in TokenModal (e.g., the "Connect GitHub
Token" header, the paragraph about token usage, "Personal Access Token" label,
placeholder "ghp_...", the scope description and link text, "Cancel", and "Save
& Continue") with localized lookups (e.g., t('tokenModal.title')) and ensure
TokenModal imports/uses the i18n hook/function (e.g., useTranslation or t) so
token, handleSave, and onClose logic remain unchanged while strings come from
resource files.

Comment on lines +52 to +78
// IndexedDB (large datastores)
async get<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string
): Promise<any | null> {
const db = await getDB();
const metadata = await db.get('metadata', `${storeName}_${key}`);

if (!metadata || Date.now() - metadata.timestamp > TTL_MS) {
return null; // Cache default/miss
}
return db.get(storeName, key);
},

async set<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string,
value: any
) {
const db = await getDB();
const tx = db.transaction([storeName, 'metadata'], 'readwrite');
await Promise.all([
tx.objectStore(storeName).put(value, key),
tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`),
tx.done,
]);
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add error handling for IndexedDB operations.

IndexedDB operations can fail (e.g., storage quota exceeded, private browsing restrictions). The current implementation doesn't handle these failures, which could cause unhandled promise rejections.

♻️ Proposed error handling
   async get<StoreName extends 'repos' | 'contributors' | 'activity'>(
     storeName: StoreName,
     key: string
   ): Promise<any | null> {
+    try {
       const db = await getDB();
       const metadata = await db.get('metadata', `${storeName}_${key}`);
       
       if (!metadata || Date.now() - metadata.timestamp > TTL_MS) {
         return null;
       }
       return db.get(storeName, key);
+    } catch (error) {
+      console.warn(`Cache get failed for ${storeName}/${key}:`, error);
+      return null; // Graceful degradation
+    }
   },

   async set<StoreName extends 'repos' | 'contributors' | 'activity'>(
     storeName: StoreName,
     key: string,
     value: any
   ) {
+    try {
       const db = await getDB();
       const tx = db.transaction([storeName, 'metadata'], 'readwrite');
       await Promise.all([
         tx.objectStore(storeName).put(value, key),
         tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`),
         tx.done,
       ]);
+    } catch (error) {
+      console.warn(`Cache set failed for ${storeName}/${key}:`, error);
+      // Continue without caching - app should still work
+    }
   },
📝 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
// IndexedDB (large datastores)
async get<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string
): Promise<any | null> {
const db = await getDB();
const metadata = await db.get('metadata', `${storeName}_${key}`);
if (!metadata || Date.now() - metadata.timestamp > TTL_MS) {
return null; // Cache default/miss
}
return db.get(storeName, key);
},
async set<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string,
value: any
) {
const db = await getDB();
const tx = db.transaction([storeName, 'metadata'], 'readwrite');
await Promise.all([
tx.objectStore(storeName).put(value, key),
tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`),
tx.done,
]);
},
// IndexedDB (large datastores)
async get<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string
): Promise<any | null> {
try {
const db = await getDB();
const metadata = await db.get('metadata', `${storeName}_${key}`);
if (!metadata || Date.now() - metadata.timestamp > TTL_MS) {
return null; // Cache default/miss
}
return db.get(storeName, key);
} catch (error) {
console.warn(`Cache get failed for ${storeName}/${key}:`, error);
return null; // Graceful degradation
}
},
async set<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string,
value: any
) {
try {
const db = await getDB();
const tx = db.transaction([storeName, 'metadata'], 'readwrite');
await Promise.all([
tx.objectStore(storeName).put(value, key),
tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`),
tx.done,
]);
} catch (error) {
console.warn(`Cache set failed for ${storeName}/${key}:`, error);
// Continue without caching - app should still work
}
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/cache.ts` around lines 52 - 78, The get and set IndexedDB
operations (functions get and set using getDB, TTL_MS, db.transaction,
tx.objectStore) lack error handling and can cause unhandled rejections; wrap the
body of both get and set in try/catch blocks, catch any errors from await
getDB(), db.get(), and the transaction Promise, log the error with contextual
info (storeName and key) and for get return null on failure, while for set log
the error and ensure the transaction is aborted/cleaned up (call tx.abort if
available) without letting the exception bubble up to callers.

Comment on lines +46 to +56
const response = await fetch(`${API_BASE}${url}`, {
headers: getHeaders(),
});

if (!response.ok) {
if (response.status === 403 || response.status === 401) {
// Trigger generic rate limit event
window.dispatchEvent(new CustomEvent('github-api-limit', { detail: response.headers }));
}
throw new GitHubApiError(response.status, `GitHub API Error: ${response.statusText}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding request timeout to prevent hanging requests.

Network requests to the GitHub API have no timeout configured. A slow or unresponsive API could leave the UI in a loading state indefinitely.

♻️ Proposed timeout with AbortController
+const FETCH_TIMEOUT_MS = 30000;
+
+async function fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+  try {
+    return await fetch(url, { ...options, signal: controller.signal });
+  } finally {
+    clearTimeout(timeoutId);
+  }
+}

   fetchWithCache: async <T>(
     url: string,
     storeName: 'repos' | 'contributors' | 'activity',
     cacheKey: string
   ): Promise<T> => {
     // ...
-    const response = await fetch(`${API_BASE}${url}`, {
+    const response = await fetchWithTimeout(`${API_BASE}${url}`, {
       headers: getHeaders(),
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 46 - 56, Add a request timeout to the
GitHub API fetch in src/services/githubApi.ts by using an AbortController:
create an AbortController before calling fetch, pass controller.signal to fetch
(alongside headers from getHeaders()), and set a timer (e.g., via setTimeout)
that calls controller.abort() after the desired timeout; clear the timer after
fetch completes. Catch the abort case (check for DOMException name 'AbortError'
or similar) and rethrow a GitHubApiError with a clear timeout message so callers
can handle it, while preserving the existing status-based logic (including the
github-api-limit event dispatch) for non-timeout failures. Ensure the timer is
cleaned up in all code paths.

Comment on lines +63 to +70
getOrgDetails: async (org: string) => {
// Standard fetch, small enough to bypass idb cache or cache separately
const response = await fetch(`${API_BASE}/orgs/${org}`, {
headers: getHeaders(),
});
if (!response.ok) throw new Error('Org not found');
return response.json();
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add input validation for organization name.

getOrgDetails doesn't validate the org parameter. An empty or malformed org name could result in unexpected API calls. Consider adding basic validation.

🛡️ Proposed validation
   getOrgDetails: async (org: string) => {
+    if (!org || typeof org !== 'string' || org.trim() === '') {
+      throw new Error('Invalid organization name');
+    }
     const response = await fetch(`${API_BASE}/orgs/${org}`, {
       headers: getHeaders(),
     });
     if (!response.ok) throw new Error('Org not found');
     return response.json();
   },

Consider applying similar validation to other methods that accept org and repo parameters.

📝 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
getOrgDetails: async (org: string) => {
// Standard fetch, small enough to bypass idb cache or cache separately
const response = await fetch(`${API_BASE}/orgs/${org}`, {
headers: getHeaders(),
});
if (!response.ok) throw new Error('Org not found');
return response.json();
},
getOrgDetails: async (org: string) => {
// Standard fetch, small enough to bypass idb cache or cache separately
if (!org || typeof org !== 'string' || org.trim() === '') {
throw new Error('Invalid organization name');
}
const response = await fetch(`${API_BASE}/orgs/${org}`, {
headers: getHeaders(),
});
if (!response.ok) throw new Error('Org not found');
return response.json();
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 63 - 70, getOrgDetails currently
calls the GitHub API without validating the org parameter; add input validation
at the start of getOrgDetails to ensure org is a non-empty string matching
GitHub org name rules (e.g. allowed chars and length, such as
/^[A-Za-z0-9-]{1,39}$/), and throw a clear error (e.g. "Invalid organization
name") before making the fetch; apply the same validation pattern to other
service methods that accept org or repo parameters to prevent malformed API
calls.

Comment on lines +72 to +97
getOrgRepos: async (org: string) => {
// Using fetchWithCache allows us to cache the entire repo list
// Note: A real implementation might need to handle pagination if an org has >100 repos.
// We'll fetch the first 100 for simplicity in this demo.
return githubApi.fetchWithCache<any[]>(
`/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`,
'repos',
org
);
},

getRepoContributors: async (org: string, repo: string) => {
return githubApi.fetchWithCache<any[]>(
`/repos/${org}/${repo}/contributors?per_page=100`,
'contributors',
`${org}_${repo}`
);
},

getRepoActivity: async (org: string, repo: string) => {
return githubApi.fetchWithCache<any[]>(
`/repos/${org}/${repo}/stats/commit_activity`,
'activity',
`${org}_${repo}`
);
}
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

Define explicit return types instead of any[].

The cached endpoint methods return Promise<any[]>, losing type information. Define interfaces matching GitHub API responses for better type safety downstream.

♻️ Proposed type definitions
+interface GitHubRepo {
+  id: number;
+  name: string;
+  full_name: string;
+  html_url: string;
+  description?: string;
+  language?: string;
+  stargazers_count: number;
+  forks_count: number;
+  open_issues_count: number;
+  pushed_at: string;
+}
+
+interface GitHubContributor {
+  login: string;
+  avatar_url: string;
+  html_url: string;
+  contributions: number;
+}
+
+interface GitHubCommitActivity {
+  week: number;
+  total: number;
+  days: number[];
+}

   getOrgRepos: async (org: string) => {
-    return githubApi.fetchWithCache<any[]>(
+    return githubApi.fetchWithCache<GitHubRepo[]>(
       `/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`,
       'repos',
       org
     );
   },

   getRepoContributors: async (org: string, repo: string) => {
-    return githubApi.fetchWithCache<any[]>(
+    return githubApi.fetchWithCache<GitHubContributor[]>(
       `/repos/${org}/${repo}/contributors?per_page=100`,
       'contributors',
       `${org}_${repo}`
     );
   },
   
   getRepoActivity: async (org: string, repo: string) => {
-    return githubApi.fetchWithCache<any[]>(
+    return githubApi.fetchWithCache<GitHubCommitActivity[]>(
       `/repos/${org}/${repo}/stats/commit_activity`,
       'activity',
       `${org}_${repo}`
     );
   }
📝 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
getOrgRepos: async (org: string) => {
// Using fetchWithCache allows us to cache the entire repo list
// Note: A real implementation might need to handle pagination if an org has >100 repos.
// We'll fetch the first 100 for simplicity in this demo.
return githubApi.fetchWithCache<any[]>(
`/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`,
'repos',
org
);
},
getRepoContributors: async (org: string, repo: string) => {
return githubApi.fetchWithCache<any[]>(
`/repos/${org}/${repo}/contributors?per_page=100`,
'contributors',
`${org}_${repo}`
);
},
getRepoActivity: async (org: string, repo: string) => {
return githubApi.fetchWithCache<any[]>(
`/repos/${org}/${repo}/stats/commit_activity`,
'activity',
`${org}_${repo}`
);
}
interface GitHubRepo {
id: number;
name: string;
full_name: string;
html_url: string;
description?: string;
language?: string;
stargazers_count: number;
forks_count: number;
open_issues_count: number;
pushed_at: string;
}
interface GitHubContributor {
login: string;
avatar_url: string;
html_url: string;
contributions: number;
}
interface GitHubCommitActivity {
week: number;
total: number;
days: number[];
}
getOrgRepos: async (org: string) => {
// Using fetchWithCache allows us to cache the entire repo list
// Note: A real implementation might need to handle pagination if an org has >100 repos.
// We'll fetch the first 100 for simplicity in this demo.
return githubApi.fetchWithCache<GitHubRepo[]>(
`/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`,
'repos',
org
);
},
getRepoContributors: async (org: string, repo: string) => {
return githubApi.fetchWithCache<GitHubContributor[]>(
`/repos/${org}/${repo}/contributors?per_page=100`,
'contributors',
`${org}_${repo}`
);
},
getRepoActivity: async (org: string, repo: string) => {
return githubApi.fetchWithCache<GitHubCommitActivity[]>(
`/repos/${org}/${repo}/stats/commit_activity`,
'activity',
`${org}_${repo}`
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 72 - 97, Replace the generic any[]
return types by defining precise response interfaces (e.g., OrgRepo,
RepoContributor, CommitActivity) that match the GitHub API shapes and use those
interfaces as the generic type parameters on githubApi.fetchWithCache and the
functions getOrgRepos, getRepoContributors, and getRepoActivity so each function
returns Promise<OrgRepo[]>, Promise<RepoContributor[]>, and
Promise<CommitActivity[]> respectively; add or import those interfaces near this
module and update the function signatures and fetchWithCache calls to use them
for proper type safety.

… to OverviewTab, sync graph edge weights, and remove unused components
@github-actions github-actions bot added size/XL and removed size/XL labels Mar 27, 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: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/dashboard/ContributorRepoGraph.tsx`:
- Around line 401-424: The graph is only operable by pointer; add
keyboard/assistive access by exposing a focusable, screen-reader-friendly
mechanism that maps canvas nodes to DOM controls and keyboard handlers: render
an offscreen or visually-hidden list of nodes (repos/contributors) with buttons
or links tied to the same handlers used by ForceGraph2D (reuse setSelectedNode,
setHoverNodeId and fgRef) and add keyboard handlers (onKeyDown) to allow arrow
navigation and Enter/Space activation to select/toggle nodes; also ensure the
ForceGraph2D wrapper has appropriate aria-label/role and updates aria-live or
aria-selected states so assistive tech sees selection changes.
- Around line 89-90: hoverNodeId and selectedNode persist across dataset
replacements causing stale hover/selection UI; add a useEffect that watches the
nodes and links props (or whatever variables hold the current dataset) and calls
setHoverNodeId(null) and setSelectedNode(null) when they change, and refactor
the click/close handlers (and any consumers) to read/write a selectedNodeId
(e.g., replace selectedNode usage with selectedNodeId state and
setSelectedNodeId) so selection is driven by the current dataset and is cleared
on data refresh; update all references to hoverNodeId, setHoverNodeId,
selectedNode, setSelectedNode, and the click/close handlers accordingly.
- Around line 257-275: The component currently hardcodes user-visible strings
(e.g., the badgeText construction using "commits", plus "Contributor",
"Repository", "Open GitHub Profile" and legend labels rendered in
ContributorRepoGraph) — replace those literals by fetching localized strings
from the app's i18n/resource module (e.g., import the shared resource or i18n.t
and use keys like repoGraph.commits, repoGraph.contributor,
repoGraph.repository, repoGraph.openProfile, repoGraph.legend.*). Update the
code locations that build badgeText (variable badgeText), the side-panel labels
and legend-rendering code (the block around lines ~458–545 mentioned) to use the
resource lookup instead of hardcoded text, and ensure the text measurement
(ctx.measureText) and canvas drawing still use the returned localized string.
- Around line 138-143: The y-force currently treats missing
GraphNode.daysSinceActive as 0 (most recent); change fg.d3Force("y") so it first
checks for null/undefined and uses a sentinel (e.g., 180 days) or another
agreed-upon value for "N/A" before clamping—e.g., days = n.daysSinceActive ==
null ? 180 : Math.max(0, Math.min(180, n.daysSinceActive)); then compute the y
as before so nodes with unknown activity are positioned consistently with the UI
that renders "N/A".
- Around line 110-115: The ResizeObserver in the ContributorRepoGraph component
forces a minimum canvas width via Math.max(520, ...) which causes clipping on
narrow viewports; remove the 520 floor so setGraphWidth(Math.floor(w)) (or
guard-null) in the useEffect/ResizeObserver callback (references: useEffect,
containerRef, ResizeObserver, setGraphWidth). Also stop hiding overflowing graph
content — change the wrapper's overflow style from "hidden" to "auto" or
"visible" (references: the container/wrapper CSS or inline style around the
graph that currently sets overflow: hidden) so the graph can scroll/be reached
on small screens.
- Line 86: The fgRef is currently typed as any and several event/draw callbacks
drop to any; replace these with explicit types from react-force-graph-2d by
creating concrete Node and Link interfaces for this component and typing the ref
as useRef<ForceGraphMethods<MyNode, MyLink> | null>, then update the
ReactForceGraph2D generic parameters and all handlers to use those types (e.g.,
nodeCanvasObject, linkCanvasObject, onNodeClick, onNodeHover, onLinkClick
signatures should use MyNode/MyLink instead of any); ensure canvas draw handlers
accept the typed 2D canvas context and the library-provided node/link parameter
shapes so TypeScript enforces correct fields and removes all remaining any
usages around fgRef and the force-graph callbacks.

In `@src/components/dashboard/ContributorsTab.tsx`:
- Line 187: The useEffect in ContributorsTab is depending on
JSON.stringify(orgNames) which is fragile; stabilize the dependency by memoizing
orgNames (e.g., create a memoized value via useMemo or derive a stable key in
state) or replace the stringified dependency with a stable reference and use a
deep-equality effect helper. Update the dependency array to reference the
memoized variable (the stableOrgNames or similar) used inside the effect, and
ensure any setters or derived values (orgNames) are updated to preserve the
stable identity.

In `@src/components/dashboard/OverviewTab.tsx`:
- Line 300: The useEffect in OverviewTab is depending on
JSON.stringify(orgNames), which is fragile; instead stabilize the dependency by
memoizing a deterministic representation of orgNames (e.g., create a memoized
string or tuple) using useMemo and depend on that memoized value in the
useEffect. Update the component (OverviewTab) to compute memoizedOrgNames =
useMemo(() => /* deterministic representation of orgNames */, [orgNames]) and
replace JSON.stringify(orgNames) with memoizedOrgNames in the useEffect
dependency array so the effect only re-runs when orgNames truly changes.
- Around line 271-273: The variable and comment are misleading:
totalCommitsLast3Months and its comment reference "commits" but
weekBuckets.reduce(...) sums fork + PRs opened + issues opened; rename the
variable and comment to reflect aggregated activity (e.g.,
totalActivityLast3Months or totalContributionsLast3Months) and update derived
avgCommitsPerWeek to avgActivityPerWeek (or similar) along with any state/prop
names and UI labels that display these values; locate usage sites by searching
for weekBuckets, totalCommitsLast3Months, and avgCommitsPerWeek and change their
names/labels to match the new, accurate terminology.
- Around line 79-86: combinedOrgData currently references validOrgs[0] and is
mutated directly; instead create a shallow copy before modifying to avoid
mutating the fetched object. Change the initialization to create a new object
(e.g., use the spread operator on validOrgs[0]) so combinedOrgData =
validOrgs.length > 0 ? { ...validOrgs[0] } : null, then set combinedOrgData.name
and combinedOrgData.description when validOrgs.length > 1; also ensure you guard
against combinedOrgData being null before assigning.

In `@src/components/dashboard/ReposTab.tsx`:
- Line 75: The useEffect inside the ReposTab component currently depends on
JSON.stringify(orgNames), which creates a new string each render and leads to
unnecessary re-runs; replace that by stabilizing the array reference (e.g.,
create memoizedOrgNames via useMemo in ReposTab that returns orgNames but only
changes when its contents change) and use memoizedOrgNames in the useEffect
dependency array (alternatively, swap to a deep-compare effect hook). Update
references to orgNames in the useEffect to use memoizedOrgNames so the effect
only runs when the actual array contents change.
🪄 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: 6d3f88dd-4441-4478-b713-0be779113def

📥 Commits

Reviewing files that changed from the base of the PR and between 2b55919 and 2ecc1a9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .gitignore
  • package.json
  • src/components/dashboard/ContributorRepoGraph.tsx
  • src/components/dashboard/ContributorsTab.tsx
  • src/components/dashboard/OverviewTab.tsx
  • src/components/dashboard/RepoDetailPanel.tsx
  • src/components/dashboard/ReposTab.tsx
  • src/components/layout/Sidebar.tsx
  • src/services/cache.ts
  • src/services/githubApi.ts


/* ─── component ──────────────────────────────────────────────────── */
export function ContributorRepoGraph({ nodes, links }: Props) {
const fgRef = useRef<any>(null);
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

🧩 Analysis chain

🏁 Script executed:

head -200 src/components/dashboard/ContributorRepoGraph.tsx | cat -n

Repository: AOSSIE-Org/OrgExplorer

Length of output: 7038


🏁 Script executed:

cat -n src/components/dashboard/ContributorRepoGraph.tsx | sed -n '120,150p; 160,180p; 340,360p; 380,400p; 410,430p'

Repository: AOSSIE-Org/OrgExplorer

Length of output: 4505


🏁 Script executed:

cat package.json | grep -A 5 -B 5 "force-graph"

Repository: AOSSIE-Org/OrgExplorer

Length of output: 360


🏁 Script executed:

npm list react-force-graph-2d 2>/dev/null | head -5

Repository: AOSSIE-Org/OrgExplorer

Length of output: 115


🌐 Web query:

react-force-graph-2d TypeScript types ForceGraphMethods

💡 Result:

react-force-graph-2d provides full TypeScript support with type definitions in dist/react-force-graph-2d.d.ts (latest version 1.29.1 as of Feb 2026). The main types are ForceGraphProps for component props and ForceGraphMethods for the ref instance methods. Key usage: import React, { useRef, useEffect } from 'react'; import ForceGraph2D from 'react-force-graph-2d'; function MyGraph { const fgRef = useRef(null); useEffect( => { if (fgRef.current) { // Access methods like: fgRef.current.d3Force('charge', d3.forceManyBody.strength(-300)); fgRef.current.zoomToFit(0, 100); } }, []); return ; } ForceGraphMethods includes: - emitParticle(link: LinkObject): ForceGraphInstance - d3Force(forceName: string, forceFn?: ForceFn): ForceFn | ForceGraphInstance - d3ReheatSimulation: ForceGraphInstance - pauseAnimation: ForceGraphInstance - resumeAnimation: ForceGraphInstance - centerAt(x?: number, y?: number, durationMs?: number): ForceGraphInstance - zoom(scale?: number, durationMs?: number): ForceGraphInstance - zoomToFit(durationMs?: number, padding?: number): ForceGraphInstance - getGraphBbox: {x: [number,number], y: [number,number]} - screen2GraphCoords(screenX: number, screenY: number): {x: number, y: number} - graph2ScreenCoords(graphX: number, graphY: number): {x: number, y: number} ForceGraphProps includes graphData, nodeId, linkSource/target, width/height, nodeRelSize/val/label/color/etc., link styling props, force config like dagMode, d3AlphaDecay, etc., event handlers. Types are generic over node/link data: ForceGraph2D<NodeObject = NodeObject, LinkObject = LinkObject>. Official repo: https://github.com/vasturiano/react-force-graph. NPM: https://www.npmjs.com/package/react-force-graph-2d. Types viewable at unpkg.com/react-force-graph-2d@1.29.1/dist/react-force-graph-2d.d.ts (or earlier versions like 1.16.1 shown).

Citations:


🌐 Web query:

react-force-graph-2d 1.29.1 NodeObject LinkObject ForceGraphMethods type definitions

💡 Result:

The type definitions for react-force-graph-2d version 1.29.1 are available in the package's dist/react-force-graph-2d.d.ts file (7.2KB). They follow the generic pattern from the underlying force-graph library and recent versions, using generic types for extensibility. NodeObject: type NodeObject<NodeType = {}> = NodeType & { id?: string | number; x?: number; y?: number; vx?: number; vy?: number; fx?: number; fy?: number; [others: string]: any; }; LinkObject: type LinkObject<NodeType = {}, LinkType = {}> = LinkType & { source?: string | number | NodeObject; target?: string | number | NodeObject; [others: string]: any; }; ForceGraphMethods (ref type, extends ForceGraphInstance from force-graph): interface ForceGraphMethods<NodeType = {}, LinkType = {}> { // Link styling emitParticle(link: LinkObject<NodeType, LinkType>): ForceGraphInstance; // Force engine configuration d3Force(forceName: 'link' | 'charge' | 'center' | string): ForceFn<NodeObject> | undefined; d3Force(forceName: 'link' | 'charge' | 'center' | string, forceFn: ForceFn<NodeObject> | null): ForceGraphInstance; d3ReheatSimulation: ForceGraphInstance; // Render control pauseAnimation: ForceGraphInstance; resumeAnimation: ForceGraphInstance; centerAt(x?: number, y?: number, durationMs?: number): ForceGraphInstance; zoom(scale?: number, durationMs?: number): ForceGraphInstance; // ... additional methods like screen2GraphCoords, etc. } The component is typed as React.FunctionComponent<ForceGraphProps<NodeType, LinkType>> with ref returning ForceGraphMethods<NodeType, LinkType>. Accessors are defined as type Accessor<In, Out> = Out | string | ((obj: In) => Out);. View the full .d.ts at https://unpkg.com/react-force-graph-2d@1.29.1/dist/react-force-graph-2d.d.ts (direct raw access). Similar to 3D version and force-graph src/index.d.ts patterns, updated for recent releases (Sep 2025-Feb 2026).

Citations:


🏁 Script executed:

cat -n src/components/dashboard/ContributorRepoGraph.tsx | sed -n '1,10p; 86p; 128p; 129p; 165p; 166p; 347p; 348p; 388p; 389p; 415p; 416p; 417p'

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1147


Replace the any types at the force-graph boundary.

The ref, force callbacks, canvas draw handlers, and node event handlers all drop to any, which removes type safety from the most mutation-heavy part of the component. Please wire these up with the library's typed ref/node/link interfaces instead. The react-force-graph-2d library (v1.29.1) provides full TypeScript support with ForceGraphMethods<NodeType, LinkType> for the ref and proper types for node/link parameters. As per coding guidelines, "TypeScript: Avoid 'any', use explicit types".

Lines affected: 86, 128–129, 165–166, 347–348, 388–389, 415–417

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ContributorRepoGraph.tsx` at line 86, The fgRef is
currently typed as any and several event/draw callbacks drop to any; replace
these with explicit types from react-force-graph-2d by creating concrete Node
and Link interfaces for this component and typing the ref as
useRef<ForceGraphMethods<MyNode, MyLink> | null>, then update the
ReactForceGraph2D generic parameters and all handlers to use those types (e.g.,
nodeCanvasObject, linkCanvasObject, onNodeClick, onNodeHover, onLinkClick
signatures should use MyNode/MyLink instead of any); ensure canvas draw handlers
accept the typed 2D canvas context and the library-provided node/link parameter
shapes so TypeScript enforces correct fields and removes all remaining any
usages around fgRef and the force-graph callbacks.

Comment on lines +89 to +90
const [hoverNodeId, setHoverNodeId] = useState<string | null>(null);
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
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

Reset hover and selection state when the dataset changes.

hoverNodeId and selectedNode survive nodes/links replacement. After an org/search refresh, that can leave the whole graph dimmed because the hovered id no longer exists, and the side panel can keep showing details from the previous dataset.

🧭 Suggested direction
-  const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
+  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
+  const selectedNode = useMemo(
+    () => nodes.find((node) => node.id === selectedNodeId) ?? null,
+    [nodes, selectedNodeId],
+  );
+
+  useEffect(() => {
+    if (hoverNodeId && !nodes.some((node) => node.id === hoverNodeId)) {
+      setHoverNodeId(null);
+    }
+  }, [hoverNodeId, nodes]);

Then update the click/close handlers to read and write selectedNodeId.

Also applies to: 94-105, 416-420, 427-527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 89 - 90,
hoverNodeId and selectedNode persist across dataset replacements causing stale
hover/selection UI; add a useEffect that watches the nodes and links props (or
whatever variables hold the current dataset) and calls setHoverNodeId(null) and
setSelectedNode(null) when they change, and refactor the click/close handlers
(and any consumers) to read/write a selectedNodeId (e.g., replace selectedNode
usage with selectedNodeId state and setSelectedNodeId) so selection is driven by
the current dataset and is cleared on data refresh; update all references to
hoverNodeId, setHoverNodeId, selectedNode, setSelectedNode, and the click/close
handlers accordingly.

Comment on lines +110 to +115
useEffect(() => {
if (!containerRef.current) return;
const obs = new ResizeObserver((entries) => {
const w = entries[0]?.contentRect?.width;
if (w) setGraphWidth(Math.max(520, Math.floor(w)));
});
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

Drop the 520px width floor on narrow viewports.

Line 114 forces the canvas to stay at least 520px wide, while Line 400 hides overflow. On smaller screens, that clips part of the graph and makes nodes unreachable.

📱 Suggested fix
-      if (w) setGraphWidth(Math.max(520, Math.floor(w)));
+      if (w != null) setGraphWidth(Math.floor(w));

Also applies to: 400-405

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 110 - 115,
The ResizeObserver in the ContributorRepoGraph component forces a minimum canvas
width via Math.max(520, ...) which causes clipping on narrow viewports; remove
the 520 floor so setGraphWidth(Math.floor(w)) (or guard-null) in the
useEffect/ResizeObserver callback (references: useEffect, containerRef,
ResizeObserver, setGraphWidth). Also stop hiding overflowing graph content —
change the wrapper's overflow style from "hidden" to "auto" or "visible"
(references: the container/wrapper CSS or inline style around the graph that
currently sets overflow: hidden) so the graph can scroll/be reached on small
screens.

Comment on lines +138 to +143
fg.d3Force("y")
?.y((n: GraphNode) => {
const days = Math.max(0, Math.min(180, n.daysSinceActive || 0));
return -300 + (days / 180) * 600;
})
.strength(0.4);
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

Don’t treat unknown activity as “0 days ago.”

Line 140 falls back to 0 when daysSinceActive is missing, but Lines 473-475 and 519-521 render that same case as N/A. That makes the graph place unknown-activity nodes with the most recent contributors/repos.

📊 Suggested fix
-        const days = Math.max(0, Math.min(180, n.daysSinceActive || 0));
-        return -300 + (days / 180) * 600;
+        if (n.daysSinceActive == null) return 0;
+        const days = Math.max(0, Math.min(180, n.daysSinceActive));
+        return -300 + (days / 180) * 600;

Also applies to: 470-476, 516-523

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 138 - 143,
The y-force currently treats missing GraphNode.daysSinceActive as 0 (most
recent); change fg.d3Force("y") so it first checks for null/undefined and uses a
sentinel (e.g., 180 days) or another agreed-upon value for "N/A" before
clamping—e.g., days = n.daysSinceActive == null ? 180 : Math.max(0,
Math.min(180, n.daysSinceActive)); then compute the y as before so nodes with
unknown activity are positioned consistently with the UI that renders "N/A".

Comment on lines +257 to +275
/* commits badge */
const badgeY = ay + avatarSize + 26;
const badgeText = `${formatNum(n.value)} commits`;
const badgeW = ctx.measureText(badgeText).width + 16;
const badgeH = 18;
const bx = cx - badgeW / 2;

roundRect(ctx, bx, badgeY, badgeW, badgeH, 4);
ctx.fillStyle = "rgba(140, 198, 63, 0.12)";
ctx.fill();
ctx.strokeStyle = ACCENT;
ctx.lineWidth = 0.8;
ctx.stroke();

ctx.fillStyle = ACCENT;
ctx.font = "600 9px 'Space Mono', monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(badgeText, cx, badgeY + badgeH / 2);
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

Externalize the visible copy from this component.

Strings like commits, Contributor, Repository, Open GitHub Profile, and the legend labels are hardcoded in the canvas renderer and side panel. Move them into the app’s resource files so localization stays consistent. As per coding guidelines, "Internationalization: User-visible strings should be externalized to resource files (i18n)".

Also applies to: 458-545

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 257 - 275,
The component currently hardcodes user-visible strings (e.g., the badgeText
construction using "commits", plus "Contributor", "Repository", "Open GitHub
Profile" and legend labels rendered in ContributorRepoGraph) — replace those
literals by fetching localized strings from the app's i18n/resource module
(e.g., import the shared resource or i18n.t and use keys like repoGraph.commits,
repoGraph.contributor, repoGraph.repository, repoGraph.openProfile,
repoGraph.legend.*). Update the code locations that build badgeText (variable
badgeText), the side-panel labels and legend-rendering code (the block around
lines ~458–545 mentioned) to use the resource lookup instead of hardcoded text,
and ensure the text measurement (ctx.measureText) and canvas drawing still use
the returned localized string.

return () => {
cancelled = true;
};
}, [JSON.stringify(orgNames)]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid JSON.stringify in useEffect dependency array.

Same concern as in ReposTab.tsx — using JSON.stringify(orgNames) is fragile. Consider stabilizing the reference with useMemo or storing in state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ContributorsTab.tsx` at line 187, The useEffect in
ContributorsTab is depending on JSON.stringify(orgNames) which is fragile;
stabilize the dependency by memoizing orgNames (e.g., create a memoized value
via useMemo or derive a stable key in state) or replace the stringified
dependency with a stable reference and use a deep-equality effect helper. Update
the dependency array to reference the memoized variable (the stableOrgNames or
similar) used inside the effect, and ensure any setters or derived values
(orgNames) are updated to preserve the stable identity.

Comment on lines +79 to +86
const combinedOrgData = validOrgs.length > 0 ? validOrgs[0] : null; // simplified display of org details
if (validOrgs.length > 1) {
combinedOrgData.name = validOrgs
.map((o) => o.name || o.login)
.join(" + ");
combinedOrgData.description =
"Combined statistics for multiple organizations";
}
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

Avoid mutating the fetched API response object directly.

Directly mutating combinedOrgData (which references validOrgs[0]) modifies the original object. This can cause unexpected behavior if the object is cached or used elsewhere.

🛡️ Proposed fix using spread operator
-const combinedOrgData = validOrgs.length > 0 ? validOrgs[0] : null;
-if (validOrgs.length > 1) {
-  combinedOrgData.name = validOrgs
-    .map((o) => o.name || o.login)
-    .join(" + ");
-  combinedOrgData.description =
-    "Combined statistics for multiple organizations";
-}
+const combinedOrgData = validOrgs.length > 0
+  ? validOrgs.length > 1
+    ? {
+        ...validOrgs[0],
+        name: validOrgs.map((o) => o.name || o.login).join(" + "),
+        description: "Combined statistics for multiple organizations",
+      }
+    : { ...validOrgs[0] }
+  : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/OverviewTab.tsx` around lines 79 - 86,
combinedOrgData currently references validOrgs[0] and is mutated directly;
instead create a shallow copy before modifying to avoid mutating the fetched
object. Change the initialization to create a new object (e.g., use the spread
operator on validOrgs[0]) so combinedOrgData = validOrgs.length > 0 ? {
...validOrgs[0] } : null, then set combinedOrgData.name and
combinedOrgData.description when validOrgs.length > 1; also ensure you guard
against combinedOrgData being null before assigning.

Comment on lines +271 to +273
// --- NEW: Project Velocity & Bus Factor ---
const totalCommitsLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); // Active score as proxy for velocity
const avgCommitsPerWeek = Math.round(totalCommitsLast3Months / 12);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Misleading variable/comment naming.

The comment says "commits" but totalCommitsLast3Months actually counts forks + PRs opened + issues opened. This could confuse future maintainers.

✏️ Suggested naming clarification
-// --- NEW: Project Velocity & Bus Factor ---
-const totalCommitsLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); // Active score as proxy for velocity
-const avgCommitsPerWeek = Math.round(totalCommitsLast3Months / 12);
+// --- NEW: Project Velocity & Bus Factor ---
+const totalActivityLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0);
+const avgActivityPerWeek = Math.round(totalActivityLast3Months / 12);

Also update the state and UI references accordingly.

📝 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
// --- NEW: Project Velocity & Bus Factor ---
const totalCommitsLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); // Active score as proxy for velocity
const avgCommitsPerWeek = Math.round(totalCommitsLast3Months / 12);
// --- NEW: Project Velocity & Bus Factor ---
const totalActivityLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0);
const avgActivityPerWeek = Math.round(totalActivityLast3Months / 12);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/OverviewTab.tsx` around lines 271 - 273, The
variable and comment are misleading: totalCommitsLast3Months and its comment
reference "commits" but weekBuckets.reduce(...) sums fork + PRs opened + issues
opened; rename the variable and comment to reflect aggregated activity (e.g.,
totalActivityLast3Months or totalContributionsLast3Months) and update derived
avgCommitsPerWeek to avgActivityPerWeek (or similar) along with any state/prop
names and UI labels that display these values; locate usage sites by searching
for weekBuckets, totalCommitsLast3Months, and avgCommitsPerWeek and change their
names/labels to match the new, accurate terminology.

};

fetchData();
}, [JSON.stringify(orgNames)]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid JSON.stringify in useEffect dependency array.

Same pattern as other tabs — JSON.stringify(orgNames) is fragile. Consider stabilizing with useMemo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/OverviewTab.tsx` at line 300, The useEffect in
OverviewTab is depending on JSON.stringify(orgNames), which is fragile; instead
stabilize the dependency by memoizing a deterministic representation of orgNames
(e.g., create a memoized string or tuple) using useMemo and depend on that
memoized value in the useEffect. Update the component (OverviewTab) to compute
memoizedOrgNames = useMemo(() => /* deterministic representation of orgNames */,
[orgNames]) and replace JSON.stringify(orgNames) with memoizedOrgNames in the
useEffect dependency array so the effect only re-runs when orgNames truly
changes.

}
};
fetchRepos();
}, [JSON.stringify(orgNames)]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid JSON.stringify in useEffect dependency array.

Using JSON.stringify(orgNames) as a dependency creates a new string on every render, which can cause unnecessary effect re-runs if the reference changes but content is the same. Consider using useMemo to stabilize the array reference or a custom comparison hook.

♻️ Proposed fix using useMemo
+import { useState, useEffect, useMemo } from "react";

-const orgNames = cacheService.getLastOrgs();
+const orgNames = useMemo(() => cacheService.getLastOrgs(), []);

 useEffect(() => {
   // ...
-}, [JSON.stringify(orgNames)]);
+}, [orgNames]);

Or if orgNames needs to react to external changes, consider storing it in state and updating via an event listener.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/ReposTab.tsx` at line 75, The useEffect inside the
ReposTab component currently depends on JSON.stringify(orgNames), which creates
a new string each render and leads to unnecessary re-runs; replace that by
stabilizing the array reference (e.g., create memoizedOrgNames via useMemo in
ReposTab that returns orgNames but only changes when its contents change) and
use memoizedOrgNames in the useEffect dependency array (alternatively, swap to a
deep-compare effect hook). Update references to orgNames in the useEffect to use
memoizedOrgNames so the effect only runs when the actual array contents change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment