Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@

.timeline-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.timeline-grid span { font-size: 0.75rem; font-weight: 500; color: #666; background: #fff; border: 1px solid #eee; padding: 0.3rem 0.7rem; border-radius: 0.5rem; }
.timeline-grid span.current-year { background: #fffbeb; color: #d97706; border-color: #fef3c7; font-weight: 700; }

Check warning on line 246 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Text does not meet the minimal contrast requirement with its background.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10X&open=AZ8YomJYVgokNhJLV10X&pullRequest=1984

.modal-footer { padding: 2rem 3rem 3rem; background: #f9f9f9; display: flex; gap: 1rem; border-top: 1px solid #eee; flex-shrink: 0; }
.modal-cta { flex: 1; padding: 1rem; border-radius: 1rem; font-size: 0.8rem; font-weight: 800; text-align: center; text-transform: uppercase; letter-spacing: 0.05em; transition: all 0.2s; }
Expand Down Expand Up @@ -938,7 +938,7 @@
</header>

<!-- Mobile Menu Overlay -->
<div id="mobileMenu" class="fixed inset-0 z-40 hidden md:hidden" role="dialog" aria-modal="true" aria-label="Navigation menu">

Check warning on line 941 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <dialog> instead of the dialog role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10K&open=AZ8YomJYVgokNhJLV10K&pullRequest=1984
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="toggleMenu()" onkeydown="if(event.key==='Enter'||event.key===' ')toggleMenu()" tabindex="0" role="button" aria-label="Close menu"></div>

Expand Down Expand Up @@ -1847,7 +1847,7 @@
updateThemeIcon();
})();

window.toggleTheme = function(){

Check warning on line 1850 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10L&open=AZ8YomJYVgokNhJLV10L&pullRequest=1984
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
updateThemeIcon();
Expand All @@ -1868,7 +1868,7 @@
// ══════════════════════════════════════════════
// MOBILE MENU
// ══════════════════════════════════════════════
const desktopNavMediaQuery = window.matchMedia('(min-width: 768px)');

Check warning on line 1871 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10M&open=AZ8YomJYVgokNhJLV10M&pullRequest=1984

function openMobileMenu(){
const menu = document.getElementById('mobileMenu');
Expand Down Expand Up @@ -2281,7 +2281,7 @@
container.innerHTML = badges + clearAll;
}

window.unselectLanguage = function(lang) {

Check warning on line 2284 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10N&open=AZ8YomJYVgokNhJLV10N&pullRequest=1984
selectedLanguages.delete(lang);
const pillBtn = document.querySelector(`.pill[data-lang="${lang}"]`);
if (pillBtn) {
Expand All @@ -2292,7 +2292,7 @@
applyFilters();
}

window.clearAllLanguages = function() {

Check warning on line 2295 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10O&open=AZ8YomJYVgokNhJLV10O&pullRequest=1984
selectedLanguages.clear();
document.querySelectorAll('.pill.active').forEach(p => {
p.classList.remove('active');
Expand All @@ -2305,7 +2305,7 @@
// NOTE: toggleLanguageMode removed — the matchAllLanguagesToggle checkbox
// uses a direct event listener (line ~1538) and does not use onclick=.

window.togglePill = function(el) {

Check warning on line 2308 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10P&open=AZ8YomJYVgokNhJLV10P&pullRequest=1984
const langDisplay = el.dataset.lang || '';
const langKey = langDisplay.trim().toLowerCase();
if (!langKey) return;
Expand Down Expand Up @@ -2782,7 +2782,7 @@
container.innerHTML = MILESTONES.map((m, i) => {
const isActive = i === activeIdx;
const isLast = i === MILESTONES.length - 1;
const connector = !isLast ? `<div class="w-0.5 h-10 bg-zinc-200"></div>` : '';

Check warning on line 2785 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10Q&open=AZ8YomJYVgokNhJLV10Q&pullRequest=1984
let dateStr;
try {
dateStr = m.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase();
Expand Down Expand Up @@ -2850,7 +2850,7 @@
}

// Organizational Logo Fallbacks
window.handleImgError = function(img, orgName) {

Check warning on line 2853 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10R&open=AZ8YomJYVgokNhJLV10R&pullRequest=1984
if (!img.dataset.triedClearbit) {
img.dataset.triedClearbit = 'true';
const domain = orgName.toLowerCase().replace(/[^a-z0-9]/g, '') + '.org';
Expand Down Expand Up @@ -3199,7 +3199,7 @@
// ══════════════════════════════════════════════
let prevFocusBeforeAnalytics = null;

window.openAnalytics = function() {

Check warning on line 3202 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10S&open=AZ8YomJYVgokNhJLV10S&pullRequest=1984
// Save current focus
prevFocusBeforeAnalytics = document.activeElement;

Expand All @@ -3219,7 +3219,7 @@
}, 100);
};

window.closeAn = function() {

Check warning on line 3222 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10T&open=AZ8YomJYVgokNhJLV10T&pullRequest=1984
document.getElementById('anBg')?.classList.remove('open');
document.body.style.overflow = 'auto';

Expand All @@ -3230,7 +3230,7 @@
prevFocusBeforeAnalytics = null;
};

window.closeAnEvent = function(e) {

Check warning on line 3233 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10U&open=AZ8YomJYVgokNhJLV10U&pullRequest=1984
if (e.target.id === 'anBg') {
closeAn();
}
Expand Down Expand Up @@ -3323,7 +3323,7 @@
// WATCHLIST — centralised bookmark sync helper
// ══════════════════════════════════════════════
function refreshVisibleBookmarkButtons() {
document.querySelectorAll('#orgGrid .bookmark-btn[data-bookmark-org]').forEach(btn => {
document.querySelectorAll('.bookmark-btn[data-bookmark-org]').forEach(btn => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Undocumented scope widening in refreshVisibleBookmarkButtons

This change — from '#orgGrid .bookmark-btn[data-bookmark-org]' to '.bookmark-btn[data-bookmark-org]' — is not mentioned in the PR description and is unrelated to the debounce optimization. The wider selector will now update bookmark buttons anywhere in the document (modals, watchlist panel, comparison view, etc.), not just inside #orgGrid. If that's intentional it should be documented, and it's worth confirming no bookmark buttons exist in hidden or detached DOM nodes that would trigger unexpected class mutations.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Out-of-scope change: bookmark selector broadening is unrelated to the PR's stated purpose of adding debounce to search/filter inputs. Additionally, the file lacks any debounce implementation despite the PR description claiming it was added to searchInput, mentorSearchInput, and hero search elements here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At index.html, line 3326:

<comment>Out-of-scope change: bookmark selector broadening is unrelated to the PR's stated purpose of adding debounce to search/filter inputs. Additionally, the file lacks any debounce implementation despite the PR description claiming it was added to searchInput, mentorSearchInput, and hero search elements here.</comment>

<file context>
@@ -3323,7 +3323,7 @@ <h3 class="font-headline text-lg font-bold text-on-surface mb-1 group-hover:text
   // ══════════════════════════════════════════════
   function refreshVisibleBookmarkButtons() {
-    document.querySelectorAll('#orgGrid .bookmark-btn[data-bookmark-org]').forEach(btn => {
+    document.querySelectorAll('.bookmark-btn[data-bookmark-org]').forEach(btn => {
       const isBookmarked = bookmarkedSet.has(btn.dataset.bookmarkOrg);
       btn.classList.toggle('active', isBookmarked);
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Broadened bookmark selector applies org-grid-specific classes and generic titles to unrelated controls

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At index.html, line 3326:

<comment>Broadened bookmark selector applies org-grid-specific classes and generic titles to unrelated controls</comment>

<file context>
@@ -3323,7 +3323,7 @@ <h3 class="font-headline text-lg font-bold text-on-surface mb-1 group-hover:text
   // ══════════════════════════════════════════════
   function refreshVisibleBookmarkButtons() {
-    document.querySelectorAll('#orgGrid .bookmark-btn[data-bookmark-org]').forEach(btn => {
+    document.querySelectorAll('.bookmark-btn[data-bookmark-org]').forEach(btn => {
       const isBookmarked = bookmarkedSet.has(btn.dataset.bookmarkOrg);
       btn.classList.toggle('active', isBookmarked);
</file context>
Suggested change
document.querySelectorAll('.bookmark-btn[data-bookmark-org]').forEach(btn => {
document.querySelectorAll('#orgGrid .bookmark-btn[data-bookmark-org]').forEach(btn => {

const isBookmarked = bookmarkedSet.has(btn.dataset.bookmarkOrg);
btn.classList.toggle('active', isBookmarked);
btn.classList.toggle('text-zinc-300', !isBookmarked);
Expand Down Expand Up @@ -3879,7 +3879,7 @@
return a.name.localeCompare(b.name);
}

window.clearAllFilters = function() {

Check warning on line 3882 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10V&open=AZ8YomJYVgokNhJLV10V&pullRequest=1984
document.getElementById('searchInput').value = '';
document.getElementById('categoryFilter').value = 'all';
document.getElementById('complexityFilter').value = 'all';
Expand Down Expand Up @@ -4617,7 +4617,7 @@

});

window.closeHelpModal = closeHelpModal;

Check warning on line 4620 in index.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8YomJYVgokNhJLV10W&open=AZ8YomJYVgokNhJLV10W&pullRequest=1984

});
</script>
Expand Down
20 changes: 17 additions & 3 deletions src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ globalThis.matchAllLanguages = matchAllLanguages;
globalThis.compareList = compareList;
globalThis.bookmarkedSet = bookmarkedSet;

// ══════════════════════════════════════════════
// DEBOUNCE UTILITY FOR SEARCH OPTIMIZATION
// ══════════════════════════════════════════════
function debounce(fn, delay) {
let timeoutId;
return function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
Comment on lines +41 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Cancel pending debounced filter work before immediate filter changes.

Line 2449-Line 2451 still call applyFilters() immediately, but timers queued from Line 2448 or Line 2503 stay live. Typing and then changing a select within 250ms now does two full filter/render passes back-to-back, which undercuts the perf goal of this PR.

Suggested fix
 function debounce(fn, delay) {
   let timeoutId;
-  return function debounced(...args) {
+  function debounced(...args) {
     clearTimeout(timeoutId);
-    timeoutId = setTimeout(() => fn(...args), delay);
-  };
+    timeoutId = setTimeout(() => {
+      timeoutId = undefined;
+      fn(...args);
+    }, delay);
+  }
+
+  debounced.cancel = () => {
+    clearTimeout(timeoutId);
+    timeoutId = undefined;
+  };
+
+  return debounced;
 }
   const debouncedApplyFilters = debounce(applyFilters, 250);
   const debouncedRenderMentorFinder = debounce(renderMentorFinder, 250);
+  const applyFiltersImmediately = () => {
+    debouncedApplyFilters.cancel();
+    applyFilters();
+  };

   document.getElementById('searchInput')?.addEventListener('input', debouncedApplyFilters);
-  document.getElementById('categoryFilter')?.addEventListener('change', applyFilters);
-  document.getElementById('complexityFilter')?.addEventListener('change', applyFilters);
-  document.getElementById('sortSelect')?.addEventListener('change', applyFilters);
+  document.getElementById('categoryFilter')?.addEventListener('change', applyFiltersImmediately);
+  document.getElementById('complexityFilter')?.addEventListener('change', applyFiltersImmediately);
+  document.getElementById('sortSelect')?.addEventListener('change', applyFiltersImmediately);

Also applies to: 2448-2451, 2503-2503

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 44-44: Avoid using the initial state variable in setState
Context: setTimeout(() => fn(...args), delay)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[error] 44-44: React's useState should not be directly called
Context: setTimeout(() => fn(...args), delay)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

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

In `@src/js/app.js` around lines 41 - 46, The immediate filter handlers in
applyFilters-related paths are not clearing already queued debounced work, so a
pending timer can still fire after a select change and trigger a second
filter/render pass. Update the filter-change flow around applyFilters, debounce,
and the handlers that call it so any outstanding timeout is canceled before
invoking an immediate filter update. Use the existing debounce helper’s timeout
management and the applyFilters call sites to ensure typing followed by a select
change only results in one filter/render pass.

}

const CATEGORY_META = {
science: { className: 'bg-blue-100 text-blue-700', label: 'Science' },
programming: { className: 'bg-violet-100 text-violet-700', label: 'Programming' },
Expand Down Expand Up @@ -2431,11 +2442,14 @@ document.addEventListener('DOMContentLoaded', () => {
renderGoodFirstIssues();

// Wire up filter event listeners
document.getElementById('searchInput')?.addEventListener('input', applyFilters);
const debouncedApplyFilters = debounce(applyFilters, 250);
const debouncedRenderMentorFinder = debounce(renderMentorFinder, 250);

document.getElementById('searchInput')?.addEventListener('input', debouncedApplyFilters);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Debouncing here won't reduce per-keystroke work if these inputs still have direct immediate handlers elsewhere; in that case each keystroke triggers both an immediate run and a delayed run. Remove or replace the immediate listeners so only the debounced path executes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/js/app.js, line 2448:

<comment>Debouncing here won't reduce per-keystroke work if these inputs still have direct immediate handlers elsewhere; in that case each keystroke triggers both an immediate run and a delayed run. Remove or replace the immediate listeners so only the debounced path executes.</comment>

<file context>
@@ -2431,11 +2442,14 @@ document.addEventListener('DOMContentLoaded', () => {
+  const debouncedApplyFilters = debounce(applyFilters, 250);
+  const debouncedRenderMentorFinder = debounce(renderMentorFinder, 250);
+
+  document.getElementById('searchInput')?.addEventListener('input', debouncedApplyFilters);
   document.getElementById('categoryFilter')?.addEventListener('change', applyFilters);
   document.getElementById('complexityFilter')?.addEventListener('change', applyFilters);
</file context>

document.getElementById('categoryFilter')?.addEventListener('change', applyFilters);
document.getElementById('complexityFilter')?.addEventListener('change', applyFilters);
document.getElementById('sortSelect')?.addEventListener('change', applyFilters);
document.getElementById('mentorSearchInput')?.addEventListener('input', renderMentorFinder);
document.getElementById('mentorSearchInput')?.addEventListener('input', debouncedRenderMentorFinder);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
document.getElementById('mentorChannelFilter')?.addEventListener('change', renderMentorFinder);
document.getElementById('matchAllLanguagesToggle')?.addEventListener('change', (e) => {
matchAllLanguages = e.target.checked;
Expand Down Expand Up @@ -2486,7 +2500,7 @@ document.addEventListener('DOMContentLoaded', () => {
searchInput.value = e.target.value;
const orgsSec = document.getElementById('orgs');
if (orgsSec) orgsSec.scrollIntoView({ behavior: 'smooth' });
applyFilters();
debouncedApplyFilters();
}
});
}
Expand Down
87 changes: 78 additions & 9 deletions src/js/githubAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,90 @@
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: This file contains substantial GitHub API fallback logic changes that are unrelated to the PR's stated goal of adding debounce to search inputs. The PR title and description exclusively discuss debounce performance improvements, yet this file adds a new direct client-side GitHub API client (fetchUserProfileDirect), restructures fetchUserProfileFromAPI with fallback error-path semantics, and changes export behavior. These unrelated functional changes increase review burden and regression risk and should be split into a separate PR.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/js/githubAnalyzer.js, line 46:

<comment>This file contains substantial GitHub API fallback logic changes that are unrelated to the PR's stated goal of adding debounce to search inputs. The PR title and description exclusively discuss debounce performance improvements, yet this file adds a new direct client-side GitHub API client (fetchUserProfileDirect), restructures fetchUserProfileFromAPI with fallback error-path semantics, and changes export behavior. These unrelated functional changes increase review burden and regression risk and should be split into a separate PR.</comment>

<file context>
@@ -35,17 +35,90 @@ function setLocalCache(cache) {
+ * @param {string} normalizedUsername - Lowercase GitHub username
+ * @returns {Promise<Object>} - Profile data in the same shape as the edge proxy response
+ */
+async function fetchUserProfileDirect(normalizedUsername) {
+  const response = await fetch(
+    // Fetch up to 100 most recently updated repos (GitHub API max per_page).
</file context>

}

/**
* Directly queries the public GitHub REST API from the client browser.
* Used as a fallback when the edge proxy is unavailable or unauthenticated.
* Subject to GitHub's unauthenticated rate limit (60 req/hr per IP).
*
* @param {string} normalizedUsername - Lowercase GitHub username
* @returns {Promise<Object>} - Profile data in the same shape as the edge proxy response
*/
async function fetchUserProfileDirect(normalizedUsername) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Propagate AbortSignal through fetchUserProfileDirect and both fallback call sites; otherwise fallback GitHub API requests cannot be cancelled, so stale lookups can continue in-flight after a newer request supersedes them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/js/githubAnalyzer.js, line 46:

<comment>Propagate `AbortSignal` through `fetchUserProfileDirect` and both fallback call sites; otherwise fallback GitHub API requests cannot be cancelled, so stale lookups can continue in-flight after a newer request supersedes them.</comment>

<file context>
@@ -35,17 +35,90 @@ function setLocalCache(cache) {
+ * @param {string} normalizedUsername - Lowercase GitHub username
+ * @returns {Promise<Object>} - Profile data in the same shape as the edge proxy response
+ */
+async function fetchUserProfileDirect(normalizedUsername) {
+  const response = await fetch(
+    // Fetch up to 100 most recently updated repos (GitHub API max per_page).
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Custom agent: Flag AI Slop and Fabricated Changes

PR description claims debounce utility added at line 40-46, but the actual code adds fetchUserProfileDirect (GitHub API fallback) in that exact line range with no debounce logic anywhere. This is a fabricated claim about implemented behavior under Rule 5.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/js/githubAnalyzer.js, line 46:

<comment>PR description claims debounce utility added at line 40-46, but the actual code adds `fetchUserProfileDirect` (GitHub API fallback) in that exact line range with no debounce logic anywhere. This is a fabricated claim about implemented behavior under Rule 5.</comment>

<file context>
@@ -35,17 +35,90 @@ function setLocalCache(cache) {
+ * @param {string} normalizedUsername - Lowercase GitHub username
+ * @returns {Promise<Object>} - Profile data in the same shape as the edge proxy response
+ */
+async function fetchUserProfileDirect(normalizedUsername) {
+  const response = await fetch(
+    // Fetch up to 100 most recently updated repos (GitHub API max per_page).
</file context>

const response = await fetch(
// Fetch up to 100 most recently updated repos (GitHub API max per_page).
// Note: Users with >100 repos will have incomplete profile data.
`https://api.github.com/users/${encodeURIComponent(normalizedUsername)}/repos?per_page=100&sort=updated`,
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'gsoc-org-finder',
},
}
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment on lines +46 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

fetchUserProfileDirect ignores the abort signal, so cancellation isn't honored on the fallback path.

fetchUserProfileFromAPI passes signal to the edge-proxy fetch, but the fallback to fetchUserProfileDirect (Lines 115 and 121) drops it. The direct fetch here runs without a signal, so once the analyzer falls back, an in-flight request can no longer be aborted. The downstream analyzeProfile in recommendation-ui.js relies on AbortError propagation to cancel stale requests, so this fallback path can leak a non-cancellable request and resolve after the consumer has moved on.

🔧 Propagate the signal into the direct fetch
-async function fetchUserProfileDirect(normalizedUsername) {
+async function fetchUserProfileDirect(normalizedUsername, signal) {
   const response = await fetch(
     // Fetch up to 100 most recently updated repos (GitHub API max per_page).
     // Note: Users with >100 repos will have incomplete profile data.
     `https://api.github.com/users/${encodeURIComponent(normalizedUsername)}/repos?per_page=100&sort=updated`,
     {
       headers: {
         Accept: 'application/vnd.github+json',
         'X-GitHub-Api-Version': '2022-11-28',
         'User-Agent': 'gsoc-org-finder',
       },
+      signal,
     }
   );

And update both call sites:

-    return await fetchUserProfileDirect(normalizedUsername);
+    return await fetchUserProfileDirect(normalizedUsername, signal);
📝 Committable suggestion

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

Suggested change
async function fetchUserProfileDirect(normalizedUsername) {
const response = await fetch(
// Fetch up to 100 most recently updated repos (GitHub API max per_page).
// Note: Users with >100 repos will have incomplete profile data.
`https://api.github.com/users/${encodeURIComponent(normalizedUsername)}/repos?per_page=100&sort=updated`,
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'gsoc-org-finder',
},
}
);
async function fetchUserProfileDirect(normalizedUsername, signal) {
const response = await fetch(
// Fetch up to 100 most recently updated repos (GitHub API max per_page).
// Note: Users with >100 repos will have incomplete profile data.
`https://api.github.com/users/${encodeURIComponent(normalizedUsername)}/repos?per_page=100&sort=updated`,
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'gsoc-org-finder',
},
signal,
}
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/githubAnalyzer.js` around lines 46 - 58, `fetchUserProfileDirect`
drops the abort signal on the fallback path, so cancellation from
`analyzeProfile` cannot stop the in-flight GitHub request. Update
`fetchUserProfileDirect` to accept a signal parameter and pass it through to its
`fetch` call, then update the fallback call sites in `fetchUserProfileFromAPI`
so they forward the same signal when switching from the edge-proxy request to
the direct request.


if (!response.ok) {
throw new Error(`GitHub ${response.status}`);
}

const repos = await response.json();

if (!Array.isArray(repos)) {
throw new Error('GitHub API returned invalid response format');

Check warning on line 67 in src/js/githubAnalyzer.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`new Error()` is too unspecific for a type check. Use `new TypeError()` instead.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ8Yol7UVgokNhJLV10J&open=AZ8Yol7UVgokNhJLV10J&pullRequest=1984
}

let totalStars = 0;
const languageCounts = {};
const topicCounts = {};
let activeDays = 9999;

repos.forEach(r => {
if (r.fork) return;
totalStars += r.stargazers_count || 0;
if (r.language) {
languageCounts[r.language] = (languageCounts[r.language] || 0) + 1;
}
if (Array.isArray(r.topics)) {
r.topics.forEach(t => { topicCounts[t] = (topicCounts[t] || 0) + 1; });
}
if (r.pushed_at) {
const days = Math.floor((Date.now() - new Date(r.pushed_at)) / 86400000);
if (days < activeDays) activeDays = days;
}
});

const languages = Object.entries(languageCounts).sort((a, b) => b[1] - a[1]).map(x => x[0]);
const topics = Object.entries(topicCounts).sort((a, b) => b[1] - a[1]).map(x => x[0]);

let activity = 'low';
if (activeDays < 30) activity = 'high';
else if (activeDays < 90) activity = 'medium';

return { languages, topics, stars: totalStars, activity };
}

async function fetchUserProfileFromAPI(normalizedUsername, signal) {
const response = await fetch(`${USER_API_ENDPOINT}?user=${encodeURIComponent(normalizedUsername)}`, { signal });
let response;
let data;

try {
data = await response.json();
} catch {
// Handle case where response is not valid JSON
response = await fetch(`${USER_API_ENDPOINT}?user=${encodeURIComponent(normalizedUsername)}`, { signal });
try {
data = await response.json();
} catch {
// Response body is not valid JSON
}
} catch (err) {
if (err.name === 'AbortError') throw err;
// Edge proxy is unreachable (network error, no GITHUB_TOKEN configured, etc.)
// Fall back to the public GitHub API directly from the browser.
return await fetchUserProfileDirect(normalizedUsername);
}

if (!response.ok) {
throw new Error(data?.error || `Failed to fetch user data: ${response.status}`);
// Edge proxy returned an error status. Fall back to the public GitHub API
// so the recommender remains usable in local forks and unauthenticated deploys.
return await fetchUserProfileDirect(normalizedUsername);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

if (!data) {
Expand Down Expand Up @@ -131,7 +204,3 @@

// Export for global usage
globalThis.analyzeGitHubUser = analyzeGitHubUser;

if (typeof module !== 'undefined' && module.exports) {
module.exports = { analyzeGitHubUser };
}
Loading