Skip to content

Add /news/ page: live news aggregator across the political spectrum#4

Open
Mehty wants to merge 3 commits intomasterfrom
claude/news-aggregator-app-BKLCZ
Open

Add /news/ page: live news aggregator across the political spectrum#4
Mehty wants to merge 3 commits intomasterfrom
claude/news-aggregator-app-BKLCZ

Conversation

@Mehty
Copy link
Copy Markdown
Owner

@Mehty Mehty commented Mar 22, 2026

Creates news/index.html — a client-side news aggregator that fetches live RSS feeds from 6 sources (The Guardian, NPR, BBC, AP, Fox News, Washington Times) via rss2json.com, rendered in a three-column Left / Center / Right layout. Requests are staggered 700ms apart to respect the free-tier rate limit. Each source renders independently with loading skeletons and error states with retry. Styled to match the existing dark portfolio theme with column-color-coded hover accents. Also adds "News" nav links to index.html (desktop nav, mobile nav, footer nav).

https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9

Summary by CodeRabbit

  • New Features
    • Added "News" navigation links in header, mobile menu, and footer.
    • Launched News Spectrum: a responsive three-column interface aggregating headlines by perspective with per-column counts.
    • Live refresh that reloads all feeds, shows a last-updated timestamp, and prevents stale results.
    • Per-source loading placeholders, per-source error panels with retry, and clickable headline cards.
    • Enhanced interactions: hover glow, cursor-driven effects, and animated reveal on scroll.

Creates news/index.html — a client-side news aggregator that fetches live
RSS feeds from 6 sources (The Guardian, NPR, BBC, AP, Fox News, Washington
Times) via rss2json.com, rendered in a three-column Left / Center / Right
layout. Requests are staggered 700ms apart to respect the free-tier rate
limit. Each source renders independently with loading skeletons and error
states with retry. Styled to match the existing dark portfolio theme with
column-color-coded hover accents. Also adds "News" nav links to index.html
(desktop nav, mobile nav, footer nav).

https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Adds a “News” navigation link (header, mobile menu, footer) and a new /news/ page that aggregates RSS feeds into a three-column political-lean layout with per-source skeletons, headline cards, staggered fetch + refresh/retry, hover effects, and a last-updated indicator.

Changes

Cohort / File(s) Summary
Navigation Updates
index.html
Inserted "News" link into desktop header (#nav .nav-links), mobile menu (.mobile-nav), and footer (footer .footer-nav). Desktop link includes inline gradient styling.
News Page Implementation
news/index.html
New page implementing "News Spectrum": builds per-source DOM stubs, shows skeleton loaders, fetches RSS via an RSS-to-JSON API, renders headline cards (title/optional description/relative time), accumulates lean counts, handles per-source errors with retrySingle(srcId), global refreshAll() with staggered fetches and generation-counter protection, updates last-updated timestamp, and includes cursor/hover/scroll-reveal effects.

Sequence Diagram

sequenceDiagram
    participant Browser as Browser Client
    participant API as RSS-to-JSON API
    participant Feeds as RSS Feed Sources

    Browser->>Browser: Build 3-column UI and per-source stubs
    loop For each RSS source (staggered)
        Browser->>Browser: Show skeleton loader
        Browser->>API: Request feed JSON
        API->>Feeds: Fetch RSS XML
        Feeds-->>API: Return RSS XML
        API-->>Browser: Return JSON feed items
        Browser->>Browser: Render headline cards and update lean counts
    end
    Browser->>Browser: Update "last updated" timestamp

    alt User triggers Refresh (refreshAll)
        Browser->>Browser: Clear columns, rebuild stubs, increment generation
        loop Staggered per-source
            Browser->>API: Re-fetch feed
            API-->>Browser: JSON items
            Browser->>Browser: Render refreshed cards (only if generation matches)
        end
        Browser->>Browser: Update timestamp
    end

    alt Fetch error for a source
        Browser->>Browser: Replace source area with error panel + retry button
        Browser->>API: retrySingle(srcId)
        API-->>Browser: JSON items or error
        Browser->>Browser: Update or persist error state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped to a page where headlines bloom,
Left, center, right — in a three-column room,
Skeletons shimmer, cards glow on hover,
Refresh spins round, retry’s a quick rover,
A rabbit cheers — the spectrum finds its tune.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 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: adding a new /news/ page that aggregates live news feeds across the political spectrum.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/news-aggregator-app-BKLCZ

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

@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: 3

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

Inline comments:
In `@news/index.html`:
- Around line 351-356: The current truncate() and feed rendering logic treat
feed payloads as HTML and write msg/item.link directly into markup, which
enables XSS or javascript: URL injection; fix by creating DOM nodes
programmatically (use document.createElement for anchors and containers), assign
text via textContent (not innerHTML), and set href only after validating with
the URL constructor to allow only http: or https: schemes (otherwise omit or
replace with a safe placeholder); update all usages of truncate(), item.link and
msg in the rendering code to use this safe node-building and URL validation
approach so feed text is never inserted as HTML and links cannot be javascript:
or data: URLs.
- Around line 485-499: The current fetchAll/fetchSource flow lets responses from
earlier refreshes overwrite later UI and double-increment leanCounts; fix by
introducing a refresh generation token (e.g., a numeric currentRefreshGen)
incremented at start of fetchAll, capture localGen in each spawned fetch, and
pass it into fetchSource or check it when handling responses so any response
with gen !== currentRefreshGen is ignored; also collect per-source promises (map
SOURCES to fetchSourcePromise(localGen)) and await Promise.all on them before
updating the timestamp and removing the spinner (manipulate elements
'refresh-btn' and 'last-refresh' only when the awaited generation completes) so
UI flips only after that generation finishes.
- Around line 98-99: The page uses custom media queries targeting .spectrum-grid
to switch from multi-column to two-column and single-column layouts; remove
those `@media` rules and switch the layout to W3.CSS responsive classes instead:
replace the .spectrum-grid styling by using W3.CSS grid helpers (e.g., wrap
items in a w3-row and give each grid item classes like w3-third / w3-half /
w3-full or combine w3-col variants such as w3-third w3-half-small w3-full to
achieve desktop: 3 cols, <=1050px: 2 cols, <=680px: 1 col) and use w3-hide-small
/ w3-hide-medium if you need to hide/show elements at breakpoints; update the
markup where .spectrum-grid items are rendered (and the duplicated rules noted
around lines 186-190) and delete the two `@media` blocks so all responsive
behavior comes from W3.CSS classes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7ffe5d59-32a3-483d-9a3b-53caacf3bc14

📥 Commits

Reviewing files that changed from the base of the PR and between bc81987 and f76201c.

📒 Files selected for processing (2)
  • index.html
  • news/index.html

Comment thread news/index.html Outdated
Comment thread news/index.html
Comment thread news/index.html
- renderItems/renderError: build DOM nodes programmatically (no innerHTML
  for external data); set text via textContent; validate item.link with URL
  constructor, allowing only http:/https: schemes
- fetchAll/fetchSource: introduce currentRefreshGen counter incremented on
  each refresh; pass localGen into fetchSource and discard responses where
  gen !== currentRefreshGen; use Promise.all on staggered fetches so spinner
  and timestamp update only after the generation fully completes
- Layout: remove custom .spectrum-grid CSS grid rules and @media breakpoints;
  add W3.CSS CDN and use w3-row / w3-col l4 m6 s12 for responsive 3→2→1
  column layout

https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
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: 1

♻️ Duplicate comments (2)
news/index.html (2)

177-181: ⚠️ Potential issue | 🟡 Minor

Replace this remaining width breakpoint with W3.CSS classes.

This block still hard-codes responsive spacing in CSS. Please move these adjustments to W3.CSS classes/utilities so the page stays aligned with the repo’s HTML responsiveness rule.

As per coding guidelines, **/*.html: Use W3.CSS responsive classes (such as w3-hide-small) for responsive design instead of writing custom CSS media queries.

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

In `@news/index.html` around lines 177 - 181, The `@media` block hard-coding
responsive spacing should be removed and replaced with W3.CSS responsive utility
classes: remove the media query that targets `#nav`, .container, and .page-meta
and instead apply appropriate W3.CSS classes directly in the HTML (for example
add w3-padding or w3-padding-small to `#nav`, use w3-container or w3-padding on
.container, and use w3-row/w3-bar or spacing utilities for .page-meta to control
gap across breakpoints); update the HTML elements to include these classes and
delete the CSS selectors in the diff so responsiveness is driven by W3.CSS
utilities rather than custom media queries.

521-523: ⚠️ Potential issue | 🟠 Major

Make refresh clicks a no-op while a batch is already running.

Line 523 is cosmetic only; it does not stop a second click from launching another six-source batch. Two quick clicks already schedule 12 rss2json requests, so the page can blow past the “10 req/min” budget even though older results are discarded later.

Also applies to: 532-539

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

In `@news/index.html` around lines 521 - 523, The fetchAll function and its
associated refresh click handler currently only add a spinning class but do not
prevent concurrent executions; introduce a module-level/in-scope boolean flag
(e.g., isFetching) used by fetchAll to return immediately if true, set
isFetching = true at start and false in all completion paths (success or error)
so multiple rapid clicks cannot launch overlapping batches, and ensure the
refresh button's class toggling (element id 'refresh-btn') and any promise
rejection paths clear the flag so the UI and state remain consistent; apply the
same guard to the code referenced around lines 532-539 (the click handler) so
clicks become a no-op while a batch is running.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@news/index.html`:
- Around line 558-560: The retrySingle function is calling fetchSource without
the current generation token, causing the stale-response guards in fetchSource
to bail; update retrySingle to accept or obtain the active generation token and
call fetchSource with it (i.e., invoke fetchSource(src, gen) or fetchSource(src,
currentGen)), and ensure the fetchSource signature and callers support the
generation/token parameter so the stale-response checks (in fetchSource's
guards) receive the active token during retries.

---

Duplicate comments:
In `@news/index.html`:
- Around line 177-181: The `@media` block hard-coding responsive spacing should be
removed and replaced with W3.CSS responsive utility classes: remove the media
query that targets `#nav`, .container, and .page-meta and instead apply
appropriate W3.CSS classes directly in the HTML (for example add w3-padding or
w3-padding-small to `#nav`, use w3-container or w3-padding on .container, and use
w3-row/w3-bar or spacing utilities for .page-meta to control gap across
breakpoints); update the HTML elements to include these classes and delete the
CSS selectors in the diff so responsiveness is driven by W3.CSS utilities rather
than custom media queries.
- Around line 521-523: The fetchAll function and its associated refresh click
handler currently only add a spinning class but do not prevent concurrent
executions; introduce a module-level/in-scope boolean flag (e.g., isFetching)
used by fetchAll to return immediately if true, set isFetching = true at start
and false in all completion paths (success or error) so multiple rapid clicks
cannot launch overlapping batches, and ensure the refresh button's class
toggling (element id 'refresh-btn') and any promise rejection paths clear the
flag so the UI and state remain consistent; apply the same guard to the code
referenced around lines 532-539 (the click handler) so clicks become a no-op
while a batch is running.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5681315f-c56d-4765-96ec-7348e352a190

📥 Commits

Reviewing files that changed from the base of the PR and between f76201c and b73e957.

📒 Files selected for processing (1)
  • news/index.html

Comment thread news/index.html Outdated
retrySingle called fetchSource(src) without the generation token
introduced in the previous commit, so gen was undefined and the
gen !== currentRefreshGen guard always returned early. Pass
currentRefreshGen so retries render correctly.

https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
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: 3

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

Inline comments:
In `@news/index.html`:
- Around line 474-487: The fetch in fetchSource can hang; wrap each request with
an AbortController and a timeout: create an AbortController before calling
fetch(apiUrl, { signal }), start a setTimeout that calls controller.abort()
after a configurable timeout (e.g., 10s), and clear that timeout after fetch
resolves; catch AbortError in the catch block and call renderError(src, 'Request
timed out') (or append e.message) while still checking gen !== currentRefreshGen
as already done; ensure the timeout/abort logic references fetchSource, API,
ITEMS_PER_SOURCE, currentRefreshGen, renderSkeleton and renderError so the
per-source error state surfaces on timeout.
- Around line 501-515: The source blocks are appended without initial skeletons
so later columns remain blank until staggered network loaders run; in
buildColumns() (the loop over SOURCES / usage of LEAN_COLS and the created
element with id `src-${src.id}`) insert a skeleton/placeholder markup into the
`div id="src-${src.id}"` when creating the stub (e.g., basic skeleton DOM or
loader HTML/CSS classes) so every source shows a loader immediately, and update
the stagger logic to only delay the network/fetch calls (leave the DOM skeletons
rendered instantly).
- Around line 521-552: The refresh path currently allows concurrent batches;
prevent overlap by adding a simple in-flight guard: introduce a boolean flag
(e.g., fetchInProgress) or reuse currentRefreshGen semantics to detect an active
batch and early-return from refreshAll if a batch is running; set the flag
before calling buildColumns()/fetchAll(), clear it after fetchAll finishes
(await its promise) or when the batch is canceled, and also disable the refresh
button (refresh-btn) while in-flight so the UI cannot trigger additional
batches; ensure fetchAll still increments/uses currentRefreshGen and that the
flag is cleared in all completion/error paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a832cb8-93c4-4e8a-8476-f3400a9787e5

📥 Commits

Reviewing files that changed from the base of the PR and between b73e957 and 1c76e0c.

📒 Files selected for processing (1)
  • news/index.html

Comment thread news/index.html
Comment on lines +474 to +487
async function fetchSource(src, gen) {
renderSkeleton('src-' + src.id);
try {
const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`;
const res = await fetch(apiUrl);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (data.status !== 'ok') throw new Error(data.message || 'Feed error');
if (gen !== currentRefreshGen) return;
renderItems(src, data.items);
} catch (e) {
if (gen !== currentRefreshGen) return;
renderError(src, 'Could not load ' + src.name + '. ' + (e.message || ''));
}
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

Add a timeout to each feed request.

If rss2json hangs instead of erroring, this fetch() never settles and Promise.all() leaves the page spinning forever. Time out the request and surface it through the existing per-source error state.

🛠️ Minimal fix
 async function fetchSource(src, gen) {
   renderSkeleton('src-' + src.id);
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 10000);
   try {
     const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`;
-    const res = await fetch(apiUrl);
+    const res = await fetch(apiUrl, { signal: controller.signal });
     if (!res.ok) throw new Error('HTTP ' + res.status);
     const data = await res.json();
     if (data.status !== 'ok') throw new Error(data.message || 'Feed error');
     if (gen !== currentRefreshGen) return;
     renderItems(src, data.items);
   } catch (e) {
     if (gen !== currentRefreshGen) return;
     renderError(src, 'Could not load ' + src.name + '. ' + (e.message || ''));
+  } finally {
+    clearTimeout(timeoutId);
   }
 }
📝 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 fetchSource(src, gen) {
renderSkeleton('src-' + src.id);
try {
const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`;
const res = await fetch(apiUrl);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (data.status !== 'ok') throw new Error(data.message || 'Feed error');
if (gen !== currentRefreshGen) return;
renderItems(src, data.items);
} catch (e) {
if (gen !== currentRefreshGen) return;
renderError(src, 'Could not load ' + src.name + '. ' + (e.message || ''));
}
async function fetchSource(src, gen) {
renderSkeleton('src-' + src.id);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`;
const res = await fetch(apiUrl, { signal: controller.signal });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (data.status !== 'ok') throw new Error(data.message || 'Feed error');
if (gen !== currentRefreshGen) return;
renderItems(src, data.items);
} catch (e) {
if (gen !== currentRefreshGen) return;
renderError(src, 'Could not load ' + src.name + '. ' + (e.message || ''));
} finally {
clearTimeout(timeoutId);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@news/index.html` around lines 474 - 487, The fetch in fetchSource can hang;
wrap each request with an AbortController and a timeout: create an
AbortController before calling fetch(apiUrl, { signal }), start a setTimeout
that calls controller.abort() after a configurable timeout (e.g., 10s), and
clear that timeout after fetch resolves; catch AbortError in the catch block and
call renderError(src, 'Request timed out') (or append e.message) while still
checking gen !== currentRefreshGen as already done; ensure the timeout/abort
logic references fetchSource, API, ITEMS_PER_SOURCE, currentRefreshGen,
renderSkeleton and renderError so the per-source error state surfaces on
timeout.

Comment thread news/index.html
Comment on lines +501 to +515
SOURCES.forEach(src => {
const colEl = document.getElementById(LEAN_COLS[src.lean]);
if (!colEl) return;

// Create stub for this source
const block = document.createElement('div');
block.className = 'source-block';
block.innerHTML = `
<div class="source-name">
<i class="${src.icon}"></i> ${src.name}
</div>
<div id="src-${src.id}"></div>
`;
colEl.appendChild(block);
});
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

Render all source skeletons before the stagger starts.

Right now the stagger delays the loaders too, so later sources sit blank for up to 3.5 seconds. Seed each src-* stub with a skeleton in buildColumns() and keep the stagger only on the network calls.

💡 Minimal fix
       block.innerHTML = `
         <div class="source-name">
           <i class="${src.icon}"></i> ${src.name}
         </div>
         <div id="src-${src.id}"></div>
       `;
       colEl.appendChild(block);
+      renderSkeleton(`src-${src.id}`);
📝 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
SOURCES.forEach(src => {
const colEl = document.getElementById(LEAN_COLS[src.lean]);
if (!colEl) return;
// Create stub for this source
const block = document.createElement('div');
block.className = 'source-block';
block.innerHTML = `
<div class="source-name">
<i class="${src.icon}"></i> ${src.name}
</div>
<div id="src-${src.id}"></div>
`;
colEl.appendChild(block);
});
SOURCES.forEach(src => {
const colEl = document.getElementById(LEAN_COLS[src.lean]);
if (!colEl) return;
// Create stub for this source
const block = document.createElement('div');
block.className = 'source-block';
block.innerHTML = `
<div class="source-name">
<i class="${src.icon}"></i> ${src.name}
</div>
<div id="src-${src.id}"></div>
`;
colEl.appendChild(block);
renderSkeleton(`src-${src.id}`);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@news/index.html` around lines 501 - 515, The source blocks are appended
without initial skeletons so later columns remain blank until staggered network
loaders run; in buildColumns() (the loop over SOURCES / usage of LEAN_COLS and
the created element with id `src-${src.id}`) insert a skeleton/placeholder
markup into the `div id="src-${src.id}"` when creating the stub (e.g., basic
skeleton DOM or loader HTML/CSS classes) so every source shows a loader
immediately, and update the stagger logic to only delay the network/fetch calls
(leave the DOM skeletons rendered instantly).

Comment thread news/index.html
Comment on lines +521 to +552
async function fetchAll() {
const btn = document.getElementById('refresh-btn');
if (btn) btn.classList.add('spinning');

const localGen = ++currentRefreshGen;

// Stagger fetches ~700ms apart (safe for free tier: 10 req/min), then await all
const promises = SOURCES.map((src, i) =>
delay(i * 700).then(() => fetchSource(src, localGen))
);

await Promise.all(promises);

// Only update UI if no newer refresh has started
if (localGen !== currentRefreshGen) return;

const el = document.getElementById('last-refresh');
if (el) el.innerHTML = `<i class="fas fa-check-circle" style="color:var(--secondary)"></i> Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`;
if (btn) btn.classList.remove('spinning');
}

// ─────────────────────────────────────────
// Public: refresh all (rebuild + fetch)
// ─────────────────────────────────────────
function refreshAll() {
// Clear column content
Object.values(LEAN_COLS).forEach(id => {
const el = document.getElementById(id);
if (el) el.innerHTML = '';
});
buildColumns();
fetchAll();
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

Do not start a new batch while one is already in flight.

The generation token protects the DOM, but it does not protect the API quota. The initial load already schedules six requests, so one quick click on Refresh sends six more and can exceed the stated 10 req/min budget. Ignore/disable refreshes until the current batch finishes, or cancel the older batch before starting the new one.

🛠️ One straightforward guard
+ let isRefreshing = false;
+
 async function fetchAll() {
+  if (isRefreshing) return;
+  isRefreshing = true;
   const btn = document.getElementById('refresh-btn');
-  if (btn) btn.classList.add('spinning');
+  if (btn) {
+    btn.classList.add('spinning');
+    btn.disabled = true;
+  }
 
-  const localGen = ++currentRefreshGen;
-
-  // Stagger fetches ~700ms apart (safe for free tier: 10 req/min), then await all
-  const promises = SOURCES.map((src, i) =>
-    delay(i * 700).then(() => fetchSource(src, localGen))
-  );
-
-  await Promise.all(promises);
-
-  // Only update UI if no newer refresh has started
-  if (localGen !== currentRefreshGen) return;
-
-  const el = document.getElementById('last-refresh');
-  if (el) el.innerHTML = `<i class="fas fa-check-circle" style="color:var(--secondary)"></i> Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`;
-  if (btn) btn.classList.remove('spinning');
+  try {
+    const localGen = ++currentRefreshGen;
+    const promises = SOURCES.map((src, i) =>
+      delay(i * 700).then(() => fetchSource(src, localGen))
+    );
+
+    await Promise.all(promises);
+
+    if (localGen === currentRefreshGen) {
+      const el = document.getElementById('last-refresh');
+      if (el) el.innerHTML = `<i class="fas fa-check-circle" style="color:var(--secondary)"></i> Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`;
+    }
+  } finally {
+    isRefreshing = false;
+    if (btn) {
+      btn.classList.remove('spinning');
+      btn.disabled = false;
+    }
+  }
 }
 
 function refreshAll() {
+  if (isRefreshing) return;
   // Clear column content
   Object.values(LEAN_COLS).forEach(id => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@news/index.html` around lines 521 - 552, The refresh path currently allows
concurrent batches; prevent overlap by adding a simple in-flight guard:
introduce a boolean flag (e.g., fetchInProgress) or reuse currentRefreshGen
semantics to detect an active batch and early-return from refreshAll if a batch
is running; set the flag before calling buildColumns()/fetchAll(), clear it
after fetchAll finishes (await its promise) or when the batch is canceled, and
also disable the refresh button (refresh-btn) while in-flight so the UI cannot
trigger additional batches; ensure fetchAll still increments/uses
currentRefreshGen and that the flag is cleared in all completion/error paths.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants