Skip to content

Strengthen DevRadar discovery and comparison flows - #9

Merged
bagusardin25 merged 6 commits into
mainfrom
codex/devradar-product-polish
Jul 29, 2026
Merged

Strengthen DevRadar discovery and comparison flows#9
bagusardin25 merged 6 commits into
mainfrom
codex/devradar-product-polish

Conversation

@freddy-tama

@freddy-tama freddy-tama commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • move the primary search/discovery task above secondary catalogue metrics and keep it useful on phone-sized screens
  • add truthful live-vs-sample recovery messaging, clearer official-source actions, and consistent English date rendering
  • repair the broken comparison flow with explicit selection, accessible feedback, a responsive tray, and direct official links
  • expose backend cursor pagination instead of silently capping the catalogue at 50 rows, while rejecting stale search/load-more responses
  • correct AI Deals filters, preset/reset behavior, alert defaults, and submission form labels
  • drive advanced filter choices from catalogue metadata, expose missing region/eligibility controls, and make AI tag filters reach the backend
  • tolerate unavailable browser storage, honor the device theme initially, and synchronize native control/Android theme colors
  • preserve unrelated share parameters through auth and alert callback cleanup
  • improve keyboard navigation, filter and pagination semantics, and mobile filter reflow
  • make the extension preview explicit about sample data, replace fake success alerts with real calendar/bookmark behavior, and harden external navigation
  • clean frontend, extension, and backend-script static checks

Why

The previous first-use path placed large secondary stats ahead of search on mobile, comparison opened too early and discarded the user's selection, and the frontend ignored API cursors after the first 50 rows. Several controls advertised filters the API did not execute, while region and eligibility filters had state but no direct controls. Hard-coded choices drifted from the catalogue, browser-storage exceptions could crash preference flows, and some callbacks erased unrelated shared-list parameters. These issues obscured DevRadar's core loop: find an opportunity, evaluate its trust and fit, then continue on the official source.

User impact

Users reach search sooner, can distinguish sample data from live catalogue data, compare multiple hackathons without losing state, load the complete catalogue, and use accurate catalogue-driven filters. Saved preferences degrade safely in privacy-restricted browsers, mobile and dark-mode controls retain usable placement and native theming, and clear primary actions continue to official registration or claim pages.

Verification

  • npm run check — production TypeScript/Vite build and oxlint pass
  • npm run typecheck in extension/ — pass
  • npm run build and npm run build:firefox in extension/ — pass
  • python -m ruff check app tests scripts in backend/ — pass
  • python -m compileall -q app tests scripts in backend/ — pass
  • browser audit at 1280×720 and 390×844 — no horizontal filter overflow; search, dynamic filters, AI tags, detail CTA, dark mode, compare, alert defaults, callback query preservation, submission labels, and extension-preview flows exercised
  • git diff --check — pass

The PostgreSQL-backed pytest suite could not run locally because this Windows environment has no Docker/PostgreSQL service. The repository's GitHub CI provisions PostgreSQL and Redis and supplies that integration evidence on this PR.

  • Switched DevRadar discovery/search UI to catalogue-driven filterMeta, including distinct AI-offer technology/tag values and richer advanced filters (region/eligibility/AI tags), fixing meta mismatch and improving mobile-focused filter/preset behavior; hero now loads meta safely on mount and supports cursor-based “load more” without stale overwrites (monotonic request IDs + stale-response guards), with live-vs-sample recovery messaging.
  • Reworked comparison flow for correctness and accessibility: clearer selection/removal and dismiss behavior, controlled compare modal state with ARIA-live notice updates, standardized date rendering, and added per-item “next step / official destination” links.
  • Hardened frontend/extension discovery and analysis against races and adversarial inputs: per-tab pending analysis via a registry (no shared global callback), MV3-safe background API proxy (bounded paths/queries/bodies, capped response reads, safe JSON errors, optional bounded Idempotency-Key), and bounded/sanitized content extraction (truncation, caps on OG/links/visible text, malformed inputs tolerated).
  • Made bookmark/share persistence resilient and deterministic: failure-tolerant storage helpers, strict bookmark-id sanitization with import/share/file size limits, recoverable/capped bookmark import/share state, and UI changes that preserve unresolved locally-saved IDs even when listings aren’t currently loaded.
  • Strengthened backend request validation and stress-hardening: centralized public input limits (api/limits), bounded request-body middleware with clear 413/408 problem+json responses, trace-id validation, malformed-URL protection, alert confirmation rate limiting, and adversarial boundary tests (with DB/PostgreSQL integration coverage delegated to CI when Docker isn’t available).
  • Corrected/expanded filter/meta correctness on the backend: filter_meta.technologies now merges distinct AI-offer tags (distinct unnest/join through listing visibility constraints) with existing hackathon technologies, and status parsing now validates length and deduplicates parsed verification statuses.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR expands catalogue filtering and cursor-based loading, centralizes frontend storage and date formatting, adds backend request-boundary and alert protections, hardens extension runtime and scraping paths, and extends CI with stress checks.

Changes

Backend catalogue and request boundaries

Layer / File(s) Summary
Catalogue filters and metadata
backend/app/catalog/..., backend/app/api/public/..., backend/tests/api/test_search.py
Catalogue filters are normalized and bounded, status values are deduplicated, and AI-offer tags are included in filter metadata.
Request body and public validation
backend/app/api/limits.py, backend/app/main.py, backend/app/config.py, backend/app/api/public/...
Shared limits constrain request bodies, queries, paths, headers, tokens, and discovery payloads; oversized bodies return 413 responses.
Alert protection and submission validation
backend/app/alerts/..., backend/app/submissions/url_policy.py, backend/tests/api/...
Alert creation gains IP/email rate limiting and bounded schemas, while malformed URLs and oversized idempotency keys return validation errors.
Backend script maintenance
backend/scripts/...
CLI kind handling, model import ordering, and seed-script help formatting are updated.

Frontend catalogue and persistence

Layer / File(s) Summary
Catalogue loading and state
src/App.tsx, src/api/catalog.ts
Filter metadata, cursor pagination, stale-request protection, offline status, bookmark fallback snapshots, URL parameter consumption, and compare notices are wired together.
Dynamic filters and pagination
src/components/HeroSection.tsx, src/components/Pagination.tsx, src/components/Header.tsx, src/components/StatsOverview.tsx, src/utils/countdown.ts
Filters derive options from metadata, pagination supports loaded/available counts and load-more errors, and module controls gain accessibility state.
Bookmark storage and import boundaries
src/api/bookmarks.ts, src/api/index.ts, src/components/BookmarksDrawer.tsx, src/utils/storage.ts
Bookmark IDs, snapshots, imports, shared links, files, and unresolved saved IDs are normalized and bounded.
Shared formatting and modal forms
src/utils/dateTime.ts, src/components/SubmitModal.tsx, src/components/AlertSubscribeModal.tsx, index.html
Date and storage helpers are centralized, modal lifecycle handling is cleaned up, form associations are explicit, and theme metadata is added.
Catalogue cards and modal destinations
src/components/AIDealCard.tsx, src/components/HackathonCard.tsx, src/components/CompareModal.tsx, src/components/DetailModal.tsx
Shared date formatting, official destination links, comparison links, and updated detail actions are added.

Extension runtime and scraping hardening

Layer / File(s) Summary
Per-tab analysis and proxy boundaries
extension/src/background/...
Concurrent tab analyses use a registry, while API proxy paths, requests, responses, and JSON handling are bounded and validated.
Scraping and persisted-state limits
extension/src/content/index.ts, extension/src/shared/storage.ts
Extracted page data and extension settings/history are sanitized, truncated, deduplicated, and capped.
Extension preview and panel wiring
extension/src/sidepanel/App.tsx, src/components/ChromeExtensionSidePanel.tsx, extension/src/shared/extractor.ts, extension/src/vite-env.d.ts
Preview tab loading, result props, organizer matching, secure page opening, calendar export, save feedback, and modal accessibility are updated.

CI and stress checks

Layer / File(s) Summary
Frontend and extension check gates
.github/workflows/ci.yml, Makefile, scripts/check.*, package.json, extension/package.json, .gitignore
Frontend and extension stress, typecheck, build, and lint commands are integrated into local and CI workflows, with Playwright output ignored.
Adversarial stress harnesses
tools/frontend-stress.mjs, extension/tools/stress-tests.mjs
Bookmark parsing, ID sanitization, pending analysis, and extension storage behavior are exercised through runtime assertions.

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

@bagusardin25
bagusardin25 marked this pull request as ready for review July 28, 2026 13:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (3)
backend/tests/api/test_search.py-186-187 (1)

186-187: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use unique sentinel values in the assertions.

Fixed values such as "Rust" and "inference" may already exist in a reused test database, allowing these assertions to pass even if the new metadata query is broken. Generate a per-test suffix and assert the exact seeded values.

As per path instructions, backend tests use a real Postgres database and should avoid assertions that can pass because of pre-existing data.

Source: Path instructions

src/components/ChromeExtensionSidePanel.tsx-41-49 (1)

41-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset doesn't cancel the in-flight analyze timer.

If "Reset Simulator" (Line 227) is clicked while analyzing is true, only analyzed/feedback are cleared — analyzing stays true and analyzeTimer.current keeps running. The pending timeout later fires setAnalyzed(true), silently flipping the panel back to the "results" view after the user explicitly reset it.

🐛 Proposed fix
+  const resetSimulator = () => {
+    if (analyzeTimer.current !== null) {
+      window.clearTimeout(analyzeTimer.current);
+      analyzeTimer.current = null;
+    }
+    setAnalyzing(false);
+    setAnalyzed(false);
+    setFeedback(null);
+  };
-        <button type="button" onClick={() => { setAnalyzed(false); setFeedback(null); }} className="text-[`#FF5A36`] font-extrabold hover:underline">
+        <button type="button" onClick={resetSimulator} className="text-[`#FF5A36`] font-extrabold hover:underline">

Also applies to: 227-227

src/components/ChromeExtensionSidePanel.tsx-51-74 (1)

51-74: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a real export timestamp and delay object URL cleanup

DTSTAMP should be the current export time, not a fixed literal. Also move URL.revokeObjectURL(url) off the immediate click path; revoking in the same tick can cancel the download in some browsers.

🧹 Nitpick comments (2)
src/App.tsx (2)

447-517: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

loadMoreCatalogue fetches with no AbortSignal.

fetchHackathons/fetchAIOffers calls here (lines 474-479, 488-493) omit the signal option that the initial loadCatalogue fetch always passes. If the component unmounts (or a filter change fires a fresh loadCatalogue) while a "load more" request is in flight, the request-id guard prevents a stale state update, but the underlying network request keeps running to completion unnecessarily.

♻️ Wire an AbortController into load-more too
+      const controller = new AbortController();
       try {
         if (kind === 'hackathon') {
           const page = await fetchHackathons(queryFilters, {
             cursor,
             limit: 50,
             bookmarks,
             alerts,
+            signal: controller.signal,
           });

691-702: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

handleToggleCompare sets state from inside another setter's updater.

setCompareNotice(...) is invoked from inside the setCompareItems updater (lines 719, 723, 726-730). React's docs explicitly say updater functions must be pure and "don't try to set state from inside of them or run other side effects" — in Strict Mode this updater is invoked twice, firing the notice side effect twice. handleToggleBookmark's setBookmarkCache call inside setBookmarkIds's updater (lines 694-707) has the same shape, though that one predates this diff. The CompareModal.onRemove handler further down (already calls setCompareNotice after, not inside, setCompareItems) shows the pattern the fix should follow.

♻️ Move the notice out of the updater
 const handleToggleCompare = useCallback((hack: Hackathon) => {
-    setCompareItems((prev) => {
-      if (prev.some((i) => i.id === hack.id)) {
-        setCompareNotice(`${hack.title} removed from comparison.`);
-        return prev.filter((i) => i.id !== hack.id);
-      }
-      if (prev.length >= 3) {
-        setCompareNotice('You can compare up to 3 hackathons at a time.');
-        return prev;
-      }
-      setCompareNotice(
-        prev.length === 0
-          ? `${hack.title} added. Choose one more opportunity to compare.`
-          : `${hack.title} added to comparison.`,
-      );
-      return [...prev, hack];
-    });
-  }, []);
+    setCompareItems((prev) => {
+      if (prev.some((i) => i.id === hack.id)) return prev.filter((i) => i.id !== hack.id);
+      if (prev.length >= 3) return prev;
+      return [...prev, hack];
+    });
+    setCompareNotice((currentNotice) => {
+      // derive message from compareItems via a ref, or restructure to compute
+      // the message before calling setCompareItems using the latest known state.
+      return currentNotice;
+    });
+  }, []);

(illustrative — the cleanest fix is to compute the outcome with a ref-tracked compareItems value or accept compareItems as a dependency.)

Also applies to: 719-744


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: c447db20-677c-4c25-9bb8-cd367bb521d1

📥 Commits

Reviewing files that changed from the base of the PR and between b92fa20 and e52e795.

📒 Files selected for processing (26)
  • backend/app/catalog/service.py
  • backend/scripts/recheck_listings.py
  • backend/scripts/seed_default_sources.py
  • backend/scripts/seed_listings.py
  • backend/tests/api/test_search.py
  • extension/src/shared/extractor.ts
  • extension/src/sidepanel/App.tsx
  • extension/src/vite-env.d.ts
  • index.html
  • src/App.tsx
  • src/api/bookmarks.ts
  • src/api/catalog.ts
  • src/components/AIDealCard.tsx
  • src/components/AlertSubscribeModal.tsx
  • src/components/ChromeExtensionSidePanel.tsx
  • src/components/CompareModal.tsx
  • src/components/DetailModal.tsx
  • src/components/HackathonCard.tsx
  • src/components/Header.tsx
  • src/components/HeroSection.tsx
  • src/components/Pagination.tsx
  • src/components/StatsOverview.tsx
  • src/components/SubmitModal.tsx
  • src/utils/countdown.ts
  • src/utils/dateTime.ts
  • src/utils/storage.ts

Comment thread backend/app/catalog/service.py
Comment thread backend/tests/api/test_search.py Outdated
@freddy-tama

Copy link
Copy Markdown
Collaborator Author

Stress-hardening update (commit 7351cbe): added fixed/chunked 1 MiB request-body enforcement, bounded public search/filter/token/idempotency inputs, malformed URL protection, alert abuse throttling, capped/recoverable bookmark import/share state, tab-keyed extension analysis, bounded page/API payloads, and repeatable frontend/extension adversarial checks. Local verification: root npm run check passed; extension npm run check passed (stress + typecheck + Chrome/Firefox builds); backend ruff passed; database-free ASGI boundary and alert-limiter smoke checks passed; Playwright at 360x800 found no page/modal/drawer overflow and verified offline/rapid-input/corrupt-storage recovery. Full PostgreSQL/Redis pytest is delegated to this PR's CI because Docker is unavailable locally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
extension/src/shared/storage.ts-16-27 (1)

16-27: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

apiBaseUrl keeps query/fragment, producing malformed proxy URLs.

parsed.toString() preserves ?… and #…, so a base of https://api.example.com/?x=1 yields https://api.example.com/?x=1/api/v1/search in handleApiProxy. Strip them while normalizing.

🔧 Normalize away search/hash
       ) {
-        apiBaseUrl = parsed.toString().replace(/\/+$/, '');
+        parsed.search = '';
+        parsed.hash = '';
+        apiBaseUrl = parsed.toString().replace(/\/+$/, '');
       }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 7f05e7f8-4d5b-471c-9246-07db3f72ee39

📥 Commits

Reviewing files that changed from the base of the PR and between e52e795 and 7351cbe.

📒 Files selected for processing (40)
  • .github/workflows/ci.yml
  • .gitignore
  • Makefile
  • backend/.env.example
  • backend/.env.production.example
  • backend/app/alerts/matcher.py
  • backend/app/alerts/schemas.py
  • backend/app/alerts/service.py
  • backend/app/api/limits.py
  • backend/app/api/public/ai_offers.py
  • backend/app/api/public/alerts.py
  • backend/app/api/public/discovery.py
  • backend/app/api/public/hackathons.py
  • backend/app/api/public/search.py
  • backend/app/api/public/submissions.py
  • backend/app/catalog/search.py
  • backend/app/catalog/service.py
  • backend/app/config.py
  • backend/app/discovery/service.py
  • backend/app/main.py
  • backend/app/submissions/url_policy.py
  • backend/tests/api/test_alerts.py
  • backend/tests/api/test_discovery.py
  • backend/tests/api/test_request_limits.py
  • backend/tests/api/test_submissions.py
  • backend/tests/submissions/test_url_policy.py
  • extension/package.json
  • extension/src/background/index.ts
  • extension/src/background/pendingAnalyses.ts
  • extension/src/content/index.ts
  • extension/src/shared/storage.ts
  • extension/tools/stress-tests.mjs
  • package.json
  • scripts/check.ps1
  • scripts/check.sh
  • src/App.tsx
  • src/api/bookmarks.ts
  • src/api/index.ts
  • src/components/BookmarksDrawer.tsx
  • tools/frontend-stress.mjs

Comment thread .github/workflows/ci.yml Outdated
Comment thread backend/app/alerts/service.py
Comment thread backend/app/main.py
Comment thread extension/src/background/index.ts Outdated
Comment on lines +131 to +137
const resp = await fetch(url, {
method: options.method || 'GET',
method,
headers,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
body: serializedBody,
});

const text = await readBoundedResponse(resp, MAX_API_RESPONSE_BYTES);

Copy link
Copy Markdown

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

fetch has no timeout — a hung API can pin the proxy request indefinitely.

Every other boundary here is bounded except wall-clock time. A slow/stalled server leaves the message-response promise unsettled and the MV3 worker holding the stream.

⏱️ Add an abort signal
   const resp = await fetch(url, {
     method,
     headers,
     body: serializedBody,
+    signal: AbortSignal.timeout(15_000),
   });
📝 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
const resp = await fetch(url, {
method: options.method || 'GET',
method,
headers,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
body: serializedBody,
});
const text = await readBoundedResponse(resp, MAX_API_RESPONSE_BYTES);
const resp = await fetch(url, {
method,
headers,
body: serializedBody,
signal: AbortSignal.timeout(15_000),
});
const text = await readBoundedResponse(resp, MAX_API_RESPONSE_BYTES);

Comment thread extension/src/content/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/App.tsx (1)

757-759: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare modal only auto-closes at 0 items, not below the 2-item minimum.

The "Compare now" button is disabled below 2 items (Line 1189) and CompareModal's own onRemove closes at < 2 (Line 1212), but this effect only closes when compareItems.length === 0. Removing an item via a card's onToggleCompare (not the modal) while the modal is open can leave it open showing a single item, inconsistent with the rest of the UI's 2-item invariant.

 useEffect(() => {
-    if (compareItems.length === 0) setIsCompareOpen(false);
+    if (compareItems.length < 2) setIsCompareOpen(false);
   }, [compareItems.length]);
🧹 Nitpick comments (2)
backend/app/catalog/service.py (2)

443-499: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Public-status leak fixed; consider deduplicating the query pattern.

The join-through-Listing + PUBLIC_VISIBLE_STATUSES filter correctly resolves the prior finding where draft/needs-review tags leaked into this unauthenticated endpoint. No remaining security concern here.

Five near-identical select(func.distinct(func.unnest(...))).join(Listing, ...).where(Listing.kind == ..., Listing.verification_status.in_(public_statuses)) blocks are duplicated. Consider extracting a small helper parameterized by model/column/kind to cut duplication and reduce the chance a future column gets added without the visibility join.

♻️ Sketch of a helper to reduce duplication
async def _distinct_unnest(self, model, column, kind: ListingKind, public_statuses):
    result = await self._session.execute(
        select(func.distinct(func.unnest(column)))
        .select_from(model)
        .join(Listing, Listing.id == model.listing_id)
        .where(
            Listing.kind == kind.value,
            Listing.verification_status.in_(public_statuses),
        )
    )
    return result.scalars().all()

442-499: 🚀 Performance & Scalability | 🔵 Trivial

Consider caching filter_meta given its query cost.

Six sequential unnest-backed round trips run on every call to this unauthenticated, likely frequently-hit route (fetched on every frontend mount per src/App.tsx). Since filter metadata changes only when catalogue content changes, a short-TTL cache (or invalidate-on-write) would cut DB load without affecting data freshness materially.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 023b9a5f-07b6-4398-81d4-6614efeaf4b3

📥 Commits

Reviewing files that changed from the base of the PR and between 7351cbe and 0fce76f.

📒 Files selected for processing (4)
  • backend/app/catalog/service.py
  • backend/tests/api/test_search.py
  • src/App.tsx
  • src/components/ChromeExtensionSidePanel.tsx

Comment thread src/App.tsx Outdated
@freddy-tama
freddy-tama force-pushed the codex/devradar-product-polish branch from 7237e27 to a8e08cf Compare July 28, 2026 15:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (3)
extension/tools/stress-tests.mjs-49-60 (1)

49-60: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Cover a stalled response body, not only a stalled fetch.

This stub never returns headers, so it does not verify the critical deadline remains active while the response body is consumed. Return an OK response whose body blocks until the captured request signal aborts; otherwise a regression that clears the timer after headers would still pass.

extension/src/background/apiProxy.ts-43-49 (1)

43-49: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Idempotency key is silently dropped instead of rejected, and no other custom header is ever forwarded.

Unlike every other input check in this function (path length, query count/length), an over-200-char Idempotency-Key is just dropped (line 47) rather than throwing. A caller relying on it for POST dedup will silently lose that protection and can end up double-submitting on retries. Separately, ApiProxyOptions.headers is typed as an arbitrary Record<string, string> (see extension/src/shared/messages.ts), but this proxy only ever forwards Idempotency-Key — any other header set by a caller is silently discarded with no error.

Suggested fix
   const idempotencyKey = options.headers?.['Idempotency-Key'];
-  if (idempotencyKey && idempotencyKey.length <= 200) {
-    headers['Idempotency-Key'] = idempotencyKey;
-  }
+  if (idempotencyKey) {
+    if (idempotencyKey.length > 200) throw new Error('Idempotency-Key is too long');
+    headers['Idempotency-Key'] = idempotencyKey;
+  }
extension/src/background/apiProxy.ts-50-60 (1)

50-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

No guard against sending a body with a GET request.

serializedBody is built whenever options.body !== undefined, independent of method. If a caller passes { method: 'GET', body: {...} }, fetch is called with both method: 'GET' and a body, which throws a TypeError in browsers rather than surfacing one of this function's clean validation errors. Worth validating that a body is only allowed for non-GET methods, consistent with the other explicit checks here.

Suggested fix
   const method = (options.method || 'GET').toUpperCase();
   if (method !== 'GET' && method !== 'POST') throw new Error('Unsupported API method');
+  if (method === 'GET' && serializedBody !== undefined) {
+    throw new Error('GET requests cannot include a body');
+  }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: f5104c25-9c46-4d4e-9934-a82472fb30c3

📥 Commits

Reviewing files that changed from the base of the PR and between 0fce76f and 7237e27.

📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • backend/.env.example
  • backend/.env.production.example
  • backend/app/alerts/service.py
  • backend/app/api/public/alerts.py
  • backend/app/config.py
  • backend/app/main.py
  • backend/tests/api/test_alerts.py
  • backend/tests/api/test_request_limits.py
  • extension/src/background/apiProxy.ts
  • extension/src/background/index.ts
  • extension/src/content/index.ts
  • extension/src/content/visibleText.ts
  • extension/tools/stress-tests.mjs

Comment thread backend/app/api/public/alerts.py Outdated
@freddy-tama
freddy-tama force-pushed the codex/devradar-product-polish branch from a8e08cf to ff308cd Compare July 28, 2026 15:16
@freddy-tama

Copy link
Copy Markdown
Collaborator Author

Final hardening verification on head ff308cd: bounded fixed/chunked/slow request bodies; public-only filter metadata; no-IP email abuse throttling; strict extension proxy paths, headers, methods, body/response sizes, and end-to-end timeouts; bounded DOM extraction; rapid bookmark/compare input hardening; and immutable CI action pins. Regression evidence: root stress/build/lint passed, extension stress/typecheck/Chrome/Firefox builds passed, real-browser same-tick comparison test passed (3 selections retained; modal closed at 1), backend PostgreSQL/Redis CI passed in 57s, frontend/extension CI passed in 27s, secret scan passed, and CodeRabbit completed successfully. Branch is clean, synchronized, and mergeable.

Resolve 6 conflicts. Backend adopts main's shipped rate-limit architecture;
frontend combines both sides rather than picking a winner.

- alerts.py: main's route-level AlertSignupRateLimiter (hashed IP + email,
  honeypot) replaces the PR's in-service limiter. Kept the PR's Annotated
  token-length validation, whose typing.Annotated import sat inside the
  conflict and would otherwise have broken the module import.
- alerts/service.py: reverted to main's version. Auto-merge had left both
  limiters live, double-counting the email bucket and breaking main's
  10-per-client test against the PR's MAX_PER_IP = 5. Main's cleanup of
  abandoned unconfirmed signups is absent from the PR branch and is restored.
- test_alerts.py: main's three rate-limit tests plus the PR's two schema
  tests, which are independent of the limiter architecture.
- Header / StatsOverview: the PR's nav/section aria labels plus main's
  dark:border and SUBLINE constant. Auto-merged closing tags were already
  </nav> and </section>, so main's div openers could not be used.
- HeroSection: main's isActive/toggle chips (auto-merged code at the map
  already dereferences chip.isActive) plus the PR's live-discovery banner,
  responsive scroll container, and working live-scan trigger.
- App.tsx: union of both import sets; main's toast auto-dismiss plus the
  PR's handleToggleBookmark, which takes a fallbackItem second argument that
  a caller already passes. Dropped main's top-level StatsOverview block --
  the PR relocated it into catalogueSummary, so keeping both double-rendered
  it; carried main's loading prop across.

Verified: tsc -b, vite build, oxlint, frontend stress test, ruff, and the
full backend suite (378 passed). Chip toggle and aria wiring confirmed live
in the browser.
@bagusardin25
bagusardin25 merged commit ca05fd2 into main Jul 29, 2026
5 checks passed
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