Skip to content

Redesign add romanian - #28

Merged
flvvius merged 19 commits into
mainfrom
redesign_add_romanian
May 11, 2026
Merged

Redesign add romanian#28
flvvius merged 19 commits into
mainfrom
redesign_add_romanian

Conversation

@flvvius

@flvvius flvvius commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Activity dashboard (/activitate), authenticated & anonymous profile views, Mobile tab bar, Site footer, Language picker, Contact/About/Terms/Privacy pages
  • Navigation & Layout

    • Homepage redirects to Feed; Dashboard becomes auth entry; Bookmarks redirect to Salvate; header/nav updated to four main sections
  • Localization

    • App-wide i18n (RO/EN) for UI, meta titles, timestamps, pluralization and locale-aware routing
  • UX Improvements

    • Infinite-scroll feed, localized toasts/labels, improved auth prompts/CTAs and sign-in/sign-up flows (default redirects adjusted)

Review Change Stack

@vercel

vercel Bot commented May 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
news Error Error May 11, 2026 5:27am
news-web Ready Ready Preview, Comment May 11, 2026 5:27am

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@flvvius has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 45 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a8e888c6-fa87-4d5b-bd52-f1f87106063f

📥 Commits

Reviewing files that changed from the base of the PR and between 9347793 and 808d94b.

📒 Files selected for processing (19)
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/components/profile/AuthenticatedProfile.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/sign-up-form.tsx
  • apps/web/src/components/ui/page-loading-state.tsx
  • apps/web/src/lib/i18n/strings.ts
  • apps/web/src/lib/seo.ts
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/event.$slug.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/robots[.]txt.ts
  • apps/web/src/routes/sitemap[.]xml.ts
  • apps/web/src/routes/source.$sourceId.tsx
  • packages/backend/convex/clustering.ts
  • packages/backend/convex/config.ts
  • packages/backend/convex/crons.ts
  • packages/backend/convex/migrations.ts
  • packages/backend/convex/schema.ts
  • packages/backend/convex/vectorSearchBudget.ts

Walkthrough

This PR adds app-wide i18n and locale resolution, localizes route metadata and UI, rewires root and routes (root → /feed, /bookmarks → /salvate, /dashboard → auth entry), introduces authenticated pages (/activitate, /salvate, /profil), updates navigation chrome (header/footer/mobile tab bar), changes sign-in/up redirect defaults to /activitate, and implements backend embedding-dimension and vector-search budgeting + clustering job-state changes.

Changes

Navigation and Dashboard Restructuring

Layer / File(s) Summary
Schema & Backend
packages/backend/convex/*
Embedding dims changed to 512; eventEmbeddings gains bucket/filter fields; new tables for vector-search metrics, reservations, runs, and clustering job state; clustering pipeline refactored to respect vector-search budgets and persist job-state; vector-search budget module and migration added.
I18n Core & Strings
apps/web/src/lib/i18n/*
Adds STRINGS (ro/en), getString, Locale type, resolveLocale, getServerLocale, LocaleProvider, useLocale, useT, and getLocaleFromMatches.
Root & Router Wiring
apps/web/src/routes/__root.tsx, apps/web/src/router.tsx
beforeLoad now resolves and returns locale with auth; head uses localized site SEO; RootDocument wraps LocaleProvider and sets <html lang={locale}>; router NotFound localized.
Dates & SEO Helpers
apps/web/src/lib/dates.ts, apps/web/src/lib/seo.ts
Locale-aware formatRelativeTimestamp/formatAbsoluteTimestamp added; getSiteSeo(locale) returns localized title/description for route heads.
Routes & Redirects
apps/web/src/routes/index.tsx, apps/web/src/routes/bookmarks.tsx, apps/web/src/routes/dashboard.tsx, apps/web/src/routes/activitate.tsx, apps/web/src/routes/salvate.tsx, apps/web/src/routes/profil.tsx
/ redirects to /feed; /bookmarks redirects to /salvate; /dashboard repurposed as auth entry that redirects authed users to /activitate; new /activitate activity dashboard, /salvate saved items page, and /profil profile route added with localized heads.
Feed / Event / Source
apps/web/src/routes/feed.tsx, apps/web/src/routes/event.$slug.tsx, apps/web/src/routes/source.$sourceId.tsx
Feed gets IntersectionObserver infinite loading, topic-fallback normalization, and many localized strings; Event/Source pages localized with pluralization helper and locale-aware timestamps.
Auth UI & Profile
apps/web/src/components/sign-in-form.tsx, apps/web/src/components/sign-up-form.tsx, apps/web/src/components/SignInPrompt.tsx, apps/web/src/components/profile/*, apps/web/src/components/LanguagePicker.tsx
Sign-in/up default redirectTo changed to /activitate; SignInPrompt, AnonymousProfile, and AuthenticatedProfile added; LanguagePicker sets bv_locale cookie and optionally persists preferredLanguage via backend mutation; many auth UIs localized.
Navigation Chrome & Component Localization
apps/web/src/components/header.tsx, apps/web/src/components/layout/Footer.tsx, apps/web/src/components/layout/MobileTabBar.tsx, apps/web/src/components/*
Header links updated to /feed, /salvate, /activitate, /profil; Footer and MobileTabBar added; many components switched to useT()/useLocale() and small defensive fixes applied (BookmarkButton, ShareEventButton, EventCard, EventClaimComparison, Bias components, StreakActivityCalendar, PageLoadingState, UserMenu, etc.).
Hooks & Utilities
apps/web/src/hooks/use-scroll-visibility.ts, apps/web/src/lib/i18n/getLocaleFromMatches.ts
Adds useScrollVisibility hook, getLocaleFromMatches, pluralization and other small UI helpers (MetricCard, TopicChipList, safePositiveInt).
Seeds & Config
packages/backend/convex/seeds.ts, packages/backend/convex/config.ts
Seed defaults extended with vector-search budgeting keys; eventEmbeddings seed vector dims updated to 512 and bucket fields populated; admin mutation setTopicInferenceSettings added.
Backend: preferredLanguage
packages/backend/convex/user.ts
users.profile.preferredLanguage added to updateProfile schema; new updatePreferredLanguage mutation implemented.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • flvvius/news#7: Related header/mobile-navigation changes and mobile sheet menu.
  • flvvius/news#3: Prior SEO/i18n adjustments that intersect with localized head/meta changes.
  • flvvius/news#27: Overlapping Convex clustering and vector-search budgeting changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch redesign_add_romanian

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/components/bookmark-button.tsx (1)

46-55: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider simplifying the type guard using optional chaining.

The defensive type checking is appropriate given the alpha version of @convex-dev/react-query, but the current implementation is quite verbose. Consider using optional chaining for improved readability:

♻️ Cleaner alternative using optional chaining
-    onSuccess: (data) => {
-      if (
-        typeof data === "object" &&
-        data !== null &&
-        "bookmarked" in data &&
-        data.bookmarked === true
-      ) {
+    onSuccess: (data) => {
+      if (data?.bookmarked === true) {
         toast.success("Bookmarked");
       } else {
         toast("Bookmark removed");
       }
     },
🤖 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 `@apps/web/src/components/bookmark-button.tsx` around lines 46 - 55, Replace
the verbose defensive type guard with optional chaining to simplify the check:
instead of the multiple checks around the local variable data, use a single
condition like data?.bookmarked === true to decide between
toast.success("Bookmarked") and toast("Bookmark removed"); update the
conditional in the BookmarkButton component (the block referencing data and
toast) so behavior remains identical but the guard is concise and readable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/profile/AuthenticatedProfile.tsx`:
- Around line 34-42: The handleSignOut function currently awaits
authClient.signOut but has no error handling; wrap the call in a try/catch (or
add .catch) around authClient.signOut inside handleSignOut, keep the existing
fetchOptions.onSuccess behavior, and in the catch block log the error (e.g.,
console.error) and surface user feedback (e.g., set an error state or call a
toast/alert) so failures (network/auth errors) don’t silently fail and the page
is not reloaded on error. Reference: handleSignOut and authClient.signOut.

In `@apps/web/src/routes/dashboard.tsx`:
- Around line 63-71: The effect showing the verification toast immediately calls
replaceDashboardSearch("signin") which currently does a full
window.location.replace and tears down the Sonner toaster before the toast
renders; change the flow in the useEffect handling isVerified (and using
hasShownVerifiedToastRef) to first show the toast via toast.message and then
update the URL/search using a client-side search updater (not full reload) that
explicitly removes the verified query param (instead of implicitly dropping it),
or defer/await navigation until after the toast has had a chance to render;
update replaceDashboardSearch (or its consumer) to perform a search-only update
(e.g., using navigate or updateSearchParams) that strips verified rather than
calling window.location.replace so the toast remains visible.
- Around line 39-61: The current useEffect and replaceDashboardSearch functions
use window.location.replace which forces full reloads; change them to use
TanStack Router's navigation via useNavigate and the router search updater: in
the component, call const navigate = useNavigate() and replace the
isAuthenticated redirect in useEffect to navigate({ to: "/activitate" }, {
replace: true }) (or use the router's push with replace option), and rewrite
replaceDashboardSearch to build a URLSearchParams but call navigate({ to:
"/dashboard", search: (prev) => { const s = new URLSearchParams(prev);
s.set("mode", mode); if (search.redirect) s.set("redirect", search.redirect); if
(verified !== undefined) s.set("verified", String(verified)); return
s.toString(); } }, { replace: true }) so navigation stays client-side and the
verification toast can render.

In `@apps/web/src/routes/reset-password.tsx`:
- Line 73: Replace the hard navigation call
window.location.assign("/dashboard?mode=signin") with TanStack Router's
useNavigate to preserve SPA routing: import and call useNavigate() (e.g., const
navigate = useNavigate()) in the component and replace the assign call with
navigate({ to: "/dashboard", search: { mode: "signin" } }) or navigate({ to:
"/dashboard?mode=signin" }) to match the existing navigate usage in this file
(see the other navigate call used around line 111); ensure the useNavigate
import is added where hooks are declared.

---

Outside diff comments:
In `@apps/web/src/components/bookmark-button.tsx`:
- Around line 46-55: Replace the verbose defensive type guard with optional
chaining to simplify the check: instead of the multiple checks around the local
variable data, use a single condition like data?.bookmarked === true to decide
between toast.success("Bookmarked") and toast("Bookmark removed"); update the
conditional in the BookmarkButton component (the block referencing data and
toast) so behavior remains identical but the guard is concise and readable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c9d3cb77-62a2-4059-a0e1-59dbea6c31b2

📥 Commits

Reviewing files that changed from the base of the PR and between fc812e0 and 85a5371.

⛔ Files ignored due to path filters (1)
  • apps/web/src/routeTree.gen.ts is excluded by !**/routeTree.gen.ts
📒 Files selected for processing (26)
  • apps/web/src/components/SignInPrompt.tsx
  • apps/web/src/components/auth-prompt-banner.tsx
  • apps/web/src/components/bookmark-button.tsx
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/Footer.tsx
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/components/profile/AnonymousProfile.tsx
  • apps/web/src/components/profile/AuthenticatedProfile.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/sign-up-form.tsx
  • apps/web/src/lib/auth-redirect.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/bookmarks.tsx
  • apps/web/src/routes/contact.tsx
  • apps/web/src/routes/cum-functioneaza.tsx
  • apps/web/src/routes/dashboard.tsx
  • apps/web/src/routes/despre.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/routes/parteneri.tsx
  • apps/web/src/routes/politica-confidentialitate.tsx
  • apps/web/src/routes/profil.tsx
  • apps/web/src/routes/reset-password.tsx
  • apps/web/src/routes/salvate.tsx
  • apps/web/src/routes/sursele-noastre.tsx
  • apps/web/src/routes/termeni.tsx

Comment thread apps/web/src/components/profile/AuthenticatedProfile.tsx
Comment thread apps/web/src/routes/dashboard.tsx
Comment thread apps/web/src/routes/dashboard.tsx Outdated
Comment thread apps/web/src/routes/reset-password.tsx Outdated
@flvvius

flvvius commented May 9, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 28

Caution

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

⚠️ Outside diff range comments (6)
apps/web/src/components/user-menu.tsx (1)

21-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add error handling for sign-out failures.

The handleSignOut function has no error handling. If authClient.signOut() fails due to network issues or server errors, the user receives no feedback and may incorrectly believe they are signed out.

🛡️ Suggested fix with error handling
 const handleSignOut = async () => {
-  await authClient.signOut({
-    fetchOptions: {
-      onSuccess: () => {
-        location.reload();
-      },
-    },
-  });
+  try {
+    await authClient.signOut({
+      fetchOptions: {
+        onSuccess: () => {
+          location.reload();
+        },
+        onError: (ctx) => {
+          console.error("Sign out failed:", ctx.error);
+          // TODO: Show user-facing error message (toast/alert)
+          alert(t("auth.signOutError") || "Failed to sign out. Please try again.");
+        },
+      },
+    });
+  } catch (error) {
+    console.error("Sign out error:", error);
+    // Fallback error handling
+    alert(t("auth.signOutError") || "Failed to sign out. Please try again.");
+  }
 };
🤖 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 `@apps/web/src/components/user-menu.tsx` around lines 21 - 29, The
handleSignOut function lacks error handling; wrap the await
authClient.signOut(...) call in a try/catch inside handleSignOut, keep the
existing fetchOptions.onSuccess to call location.reload() on success, and in the
catch block surface a user-facing error (e.g., dispatch a toast/snackbar or set
an error state) and log the error (console.error or existing logger) so
network/server failures give feedback and can be diagnosed; reference
handleSignOut and authClient.signOut when making this change.
apps/web/src/components/ui/page-loading-state.tsx (1)

7-8: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default prop values prevent i18n fallbacks from executing.

The default parameter values on lines 7-8 ("Loading your page" and "Pulling in the latest view...") are hardcoded English strings that will always be used when the props are omitted. The nullish coalescing operator (??) on lines 16-17 will never trigger the translated fallbacks because title and description will never be undefined—they'll be the default English strings instead.

This defeats the purpose of the i18n implementation.

🌐 Proposed fix to enable i18n fallbacks
 export function PageLoadingState({
-  title = "Loading your page",
-  description = "Pulling in the latest view and getting things ready.",
+  title,
+  description,
   cardCount = 2,
 }: {

Also applies to: 16-17

🤖 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 `@apps/web/src/components/ui/page-loading-state.tsx` around lines 7 - 8, The
hardcoded English defaults for the component props title and description prevent
the i18n fallbacks from ever running; remove those default parameter values (or
set them to undefined) in the PageLoadingState component signature so that the
nullish coalescing used later (the title ?? ... and description ?? ...
expressions) can trigger the translated fallbacks; update the component’s props
(title, description) to be optional and rely on the existing ?? translations
instead of supplying English literals as defaults.
apps/web/src/components/share-event-button.tsx (1)

21-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Dead summary prop and duplicated title/text payload in navigator.share.

Two related issues:

  1. The summary prop is still exported in the component's public type and is still computed and passed by callers (e.g., apps/web/src/routes/event.$slug.tsx lines 289–291), but the implementation now discards it (_summary). That's an API/UX regression: the share sheet no longer carries the perspective summary at all. If discarding was intentional, remove the prop so callers stop wasting work; otherwise restore it as the share text.
  2. navigator.share({ title, text: title, ... }) sends the same string twice. Some OS share targets render title and text as separate fields and will show duplicate content.
🛠️ Suggested fix (restore summary in share text and drop the dead destructure)
 export default function ShareEventButton({
   eventId,
   interactionContext,
   slug,
   title,
-  summary: _summary,
+  summary,
   size = "default",
   className,
 }: ShareEventButtonProps) {
@@
       try {
         await navigator.share({
           title,
-          text: title,
+          text: summary ?? title,
           url: shareUrl,
         });

If discarding summary is intentional, remove it from ShareEventButtonProps and stop passing it from event.$slug.tsx.

Also applies to: 31-31, 63-66

🤖 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 `@apps/web/src/components/share-event-button.tsx` at line 21, The component
currently destructures and ignores the summary prop (exported on
ShareEventButtonProps) and calls navigator.share with duplicate values (text:
title), so either restore summary into the shared text or remove the prop
entirely; update the ShareEventButton component by removing the dead "_summary"
destructure and use the actual summary prop when building the share payload
(navigator.share({ title, text: summary ?? title, url })) so title and text are
not identical, and if you intend to drop summary instead, remove it from
ShareEventButtonProps and from callers like the code in event.$slug.tsx to stop
computing/passing it.
apps/web/src/routes/unsubscribe.tsx (1)

53-58: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

useEffect is missing unsubscribe (the mutation object) from its dependency array.

The closure captures unsubscribe.reset. While the mutation object reference is typically stable from @tanstack/react-query, exhaustive-deps lint will flag this. Either add unsubscribe to deps or destructure and depend on the stable reset callback.

🤖 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 `@apps/web/src/routes/unsubscribe.tsx` around lines 53 - 58, The useEffect
closure references unsubscribe.reset but does not include unsubscribe in the
dependency array, which can trigger exhaustive-deps lint; update the effect to
either add unsubscribe to the deps or destructure the stable callback (e.g.,
const { reset } = unsubscribe) and depend on that reset instead—modify the
effect that uses useEffect, unsubscribe.reset, setLastEmail, email, and
lastEmail so the dependency array includes the chosen stable reference
(unsubscribe or reset) to satisfy the linter and ensure correct behavior.
apps/web/src/routes/reset-password.tsx (1)

152-156: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Field validation errors lack a live-region announcement.

The per-field error <p> blocks at lines 152–156 (and 182–186) render synchronously when validation fails on submit but are not wrapped with role="alert" or aria-live. Screen reader users won't be notified of the new error. Consider rendering the error container with role="alert" (or wrapping the field group with aria-live="polite").

As per coding guidelines: "Forms must have aria-label on inputs and ARIA live regions (aria-live, role) on status messages".

🤖 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 `@apps/web/src/routes/reset-password.tsx` around lines 152 - 156, The per-field
error messages rendered from field.state.meta.errors are not announced to screen
readers; update the error markup (the <p> elements mapped from
field.state.meta.errors) to be an ARIA live region — e.g., add role="alert" (or
wrap the field error group with aria-live="polite") so assistive tech announces
validation failures, and also ensure the corresponding input elements in the
same field group include an accessible label (aria-label or linked <label>) to
satisfy the form guidelines; locate the error renderings around the
field.state.meta.errors map and the associated input components to make these
changes.
apps/web/src/components/sign-in-form.tsx (1)

16-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace hardcoded localhost fallback with app origin from SITE.url.

getPasswordResetRedirectURL() uses redirectFallbackOrigin = "http://localhost:3001" when window is undefined (server-side rendering). If password-reset emails are generated server-side, this localhost URL will be embedded in production emails. Instead of introducing a new env var, import SITE from @/lib/seo and use SITE.url for consistency with the codebase pattern:

-function getPasswordResetRedirectURL() {
-  if (typeof window === "undefined") {
-    return `${redirectFallbackOrigin}/reset-password`;
-  }
-  return `${window.location.origin}/reset-password`;
-}
-
-const redirectFallbackOrigin = "http://localhost:3001";
+import { SITE } from "@/lib/seo";
+
+function getPasswordResetRedirectURL() {
+  if (typeof window === "undefined") {
+    return `${SITE.url}/reset-password`;
+  }
+  return `${window.location.origin}/reset-password`;
+}
🤖 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 `@apps/web/src/components/sign-in-form.tsx` around lines 16 - 23, Replace the
hardcoded redirectFallbackOrigin used by getPasswordResetRedirectURL with the
app origin from SITE.url: import SITE from "@/lib/seo" (or destructure SITE.url)
and when typeof window === "undefined" return `${SITE.url}/reset-password`
instead of using redirectFallbackOrigin; remove or replace the
redirectFallbackOrigin constant so server-side rendered password-reset links use
SITE.url consistently with the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/feed/event-claim-comparison.tsx`:
- Around line 179-189: The JSX repeatedly branches on the same status values to
call t(...) causing duplication and potential label drift; refactor by
extracting a single source of truth (e.g., a mapping object or helper function
like getStatusLabel(status) inside the EventClaimComparison component or its
module) that returns the correct translation key (using the existing t function)
and replace all duplicated conditional chains (the status → t(...) logic used
for badge, title, and body — and the similar block around lines 458-481) with
calls to that helper so all places share one mapping.
- Around line 314-318: The loading status container in the EventClaimComparison
component (the div with className "flex items-center gap-3") lacks live-region
semantics; update that container to include role="status" and aria-live="polite"
(optionally aria-atomic="true") so screen readers announce the
"{t('claim.loading')}" message reliably; locate the div in
event-claim-comparison.tsx and add those attributes to the element that wraps
the spinner and loading text.

In `@apps/web/src/components/layout/MobileTabBar.tsx`:
- Around line 36-38: The current matchesPath(pathname: string, to: string) uses
pathname.startsWith(`${to}/`) which can false-positive match routes like
'/feed-archive'; update matchesPath to support an exact-only mode: add an
optional parameter (e.g., allowPrefix: boolean = false) or accept a set of
leaf-only routes and only apply the startsWith check when allowPrefix is true or
when to === '/feed'; for leaf tab targets like '/salvate', '/activitate',
'/profil' always require exact equality (pathname === to) and reserve the prefix
match for '/feed' (or other explicitly allowed parent routes) so tabs only
activate for true children of those parents.

In `@apps/web/src/components/profile/AuthenticatedProfile.tsx`:
- Around line 170-172: The "Delete account" button currently renders <Button
asChild ...><Link to="/contact">{t("profile.deleteAccount")}</Link></Button> but
routes to /contact; either change the label to match the contact flow or
implement a real deletion flow. Option A: rename the translation key used
(replace t("profile.deleteAccount") with t("profile.requestDeletion") and add a
matching i18n string) and update the Link target to a dedicated
"/request-deletion" or keep "/contact" if desired. Option B: implement a proper
self-serve deletion by replacing the Link with a button that calls a new handler
(e.g., onDeleteAccount or showDeleteConfirmation) which opens a confirmation
modal (confirmDeletionModal), then calls a backend API (e.g., deleteAccountApi
or accounts.deleteAccount) to perform deletion, logs the action to audit
(audit.log or createAuditEntry), handles errors via process/error toast, and
redirects or signs out on success; ensure to include CSRF/auth headers and a
clear UI confirmation step.
- Around line 66-69: The avatar fallback currently uses the literal "B" in
AuthenticatedProfile (see the initials || "B" expression) which leaks a brand
placeholder; replace that with a deterministic fallback using the first
character of the user's email (e.g., derive from user.email[0].toUpperCase()) or
render a neutral icon when email is missing/invalid. Update the JSX where
getInitials(displayName) result (initials) is used so it falls back to the
computed email-first-letter or the neutral icon, and ensure you reference
displayName/user.email and the initials variable in the change.
- Line 54: In the AuthenticatedProfile component replace the deprecated Tailwind
class name: update the div whose className contains "bg-gradient-to-b
from-background via-background to-muted/20" to use "bg-linear-to-b" instead of
"bg-gradient-to-b" so it matches the Tailwind v4.1 naming (keep the other
classes unchanged).

In `@apps/web/src/components/sign-in-form.tsx`:
- Around line 70-76: The toast currently shows raw server text from onError
(error.error?.message ?? error.error?.statusText ?? t("auth.signInError")),
which can leak English-only messages to other locales; update the onError
handler in sign-in-form (the onError callback that calls toast.error) to map
known server error identifiers/messages to translation keys via t(...) (e.g.,
match error.error?.code or normalized error.error?.message to keys like
"auth.invalidCredentials") and only fall back to the raw server text as a last
resort or use a generic t("auth.signInError") fallback, ensuring toast.error
always prefers localized strings.
- Around line 245-252: The spinner branch currently builds the label by
concatenating t("auth.signIn") + "..." which is brittle for i18n; add a new
translation key (e.g., "auth.signingIn") and update the component to use
t("auth.signingIn") when state.isSubmitting is true (the branch that renders
Loader2 and the label), leaving resolvedSubmitLabel for the non-submitting
branch so the UI uses a proper localized "Signing in…" string rather than
appending "..." to t("auth.signIn").

In `@apps/web/src/components/sign-up-form.tsx`:
- Line 200: The label currently builds the in-flight text by appending a literal
ellipsis to the translated create-account string
(`{t("auth.createAccount")}...`) which is not natural across locales; add a new
translation key `auth.creatingAccount` (and update locale files) and replace the
concatenation in the SignUpForm component (where `{t("auth.createAccount")}...`
is used) with `t("auth.creatingAccount")` so each locale can supply a proper
submitting phrase; ensure any related UI/state that toggles submitting uses the
new key consistently.

In `@apps/web/src/lib/i18n/LocaleContext.tsx`:
- Line 2: useT duplicates the string resolution logic from strings.ts causing
potential lookup/fallback drift; replace its internal lookup with the shared
getString helper so all resolution and fallback behavior is centralized. Locate
the useT implementation in LocaleContext.tsx and remove the manual
STRINGS[locale][key] checks, calling getString(locale, key) (or the exported
helper name from strings.ts) instead; ensure types still use Locale and
StringKey and update the other duplicated resolution spots (noted around lines
25-27) to also delegate to getString so fallback behavior remains consistent
across the app.

In `@apps/web/src/lib/i18n/resolveLocale.ts`:
- Around line 31-39: The current Accept-Language parsing in resolveLocale (the
codes mapping and loop that checks SUPPORTED and returns a Locale) ignores q=
weights and uses header order; update the logic to parse each header part's
quality value (parse the ;q=... token, defaulting to 1), map to the primary
language subtag as you already do, build an array of {code, weight}, sort that
array by weight descending, then iterate the sorted list to return the first
entry where SUPPORTED.includes(code as Locale). Ensure you still filter out
falsy codes and preserve casting to Locale when returning.

In `@apps/web/src/routes/__root.tsx`:
- Around line 134-145: The empty catch around the call to
ctx.context.convexQueryClient.serverHttpClient?.query(api.user.getCurrentUser,
{}) swallows real errors—capture the error (e.g. catch (err)) and log it at
debug/warn level before falling back to userPreference = null so ops can see
Convex/auth/schema failures; for example, replace the empty catch with a catch
that logs a clear message and the error (e.g. console.warn or your app logger)
and then sets userPreference = null, referencing the serverHttpClient.query and
api.user.getCurrentUser call.
- Around line 109-129: The client-branch in beforeLoad currently always calls
getServerLocale(...) (with userPreference: null) causing a server round-trip on
every navigation; change it to reuse the SSR-resolved locale when unchanged by
comparing getExistingAuthContext(ctx.matches).locale to the current client
cookie or ?lang= param and only call getServerLocale/resolveLocale when the
bv_locale cookie or query lang differs (or when the signed-in user's
preferredLanguage changed), and ensure you read the actual userPreference for
signed-in users instead of hardcoding null; also wire the LanguagePicker to
explicitly invalidate or update the cached client-side locale/store when it
flips the cookie so navigation doesn't trigger unnecessary server hits.

In `@apps/web/src/routes/activitate.tsx`:
- Around line 127-136: The effect using useEffect reads fields from
currentSettings but lists their optional-chained fields in the dependency array;
replace those dependencies with a single currentSettings (or stable destructured
values) so the effect reliably runs when the settings object changes. Update the
dependency array for the useEffect that calls setMinScoreInput,
setConfidenceRatioInput, and setMaxTopicsInput to depend on currentSettings (or
on explicitly memoized/destructured stable values) instead of
currentSettings?.minScore/currentSettings?.confidenceRatio/currentSettings?.maxTopics.
- Around line 712-716: The save-config feedback in activitate.tsx (the JSX that
renders {configMessage}) isn't exposed to assistive tech; update the element
that renders configMessage (used after handleSaveConfig) to be an ARIA live
region by adding attributes such as role="status" and aria-live="polite" (or
aria-live="assertive" for errors) so screen readers announce success/error;
ensure the same wrapper element remains visually unchanged and only augments
accessibility attributes.
- Around line 740-751: The map callbacks for topics use `(t) => t.displayName`
which shadows the `t` returned by `useT()` in the rendering scope; update the
two calls that pass topics (the event.attachedTopics.map and
event.inferredTopics.map used when rendering TopicChipList) to use a different
parameter name (e.g., `topic` or `topicItem`) so they no longer shadow `useT()`
(which is referenced as `t` in AuthorizedDashboard/TopicChipList scope).
- Around line 428-433: Replace the hardcoded "Feed" text inside the Link between
Button and ChevronRight with a translated string; locate the JSX snippet using
Button, Link and ChevronRight in activitate.tsx and change the literal to a call
to the i18n translator (e.g., t("activity.feedLink") or reuse an existing nav
key like t("nav.feed")), ensuring the translator import/context is used the same
way as adjacent labels.
- Around line 30-40: formatReadDuration and formatScrollDepth return hardcoded
English suffixes; change them to use translations (either accept a t parameter
or call useT inside) and return t(...) with placeholders instead of literal "s",
"min", or "depth". Update all callers to pass the translation function (t) from
useT() when invoking formatReadDuration and formatScrollDepth (the route
component already has access to t), and add translation keys like
"read.duration.seconds", "read.duration.minutes" and "scroll.depth" with
appropriate placeholders so the functions produce localized strings.

In `@apps/web/src/routes/event`.$slug.tsx:
- Around line 345-360: The current inline ternary pluralization in
event.$slug.tsx (the articles and sourceCount spans) only handles two plural
forms; replace it with a small helper that uses Intl.PluralRules to pick a
plural category and look up a locale-aware key (e.g., for base keys
"event.articles" and "event.sourceCount" resolve keys like "event.articles.one",
"event.articles.few", "event.articles.many" etc.), then call t(resolvedKey, {
count }) — implement a helper function (e.g., getPluralKey(count, baseKey) or
selectPluralKey) and use it for both articles and sourceCount so additional
plural categories per-locale are handled automatically.

In `@apps/web/src/routes/feed.tsx`:
- Around line 397-425: The IntersectionObserver effect is recreating whenever
the unstable loadMore function identity changes; to fix it create a stable ref
for it (e.g., const loadMoreRef = useRef(loadMore)), update that ref whenever
loadMore changes (useEffect(() => { loadMoreRef.current = loadMore; },
[loadMore])) and inside the observer callback call loadMoreRef.current(pageSize)
instead of loadMore; then remove loadMore from the dependency array of the
observer effect (keep canLoadMore and pageSize) so observer churn stops while
still respecting isLoadingMoreRef and loadMoreTriggerRef.

In `@apps/web/src/routes/profil.tsx`:
- Around line 11-23: Extract the duplicated locale resolution into a helper
named getLocaleFromMatches(matches) (placed alongside Locale/LocaleContext in
the i18n module) and replace the 7-line inline resolver inside each route head
(e.g., head in profil.tsx and other routes) with a call to that helper; ensure
getLocaleFromMatches accepts the matches array, checks matches[0]?.context for a
locale property of type Locale ("ro"|"en") and returns that locale or the
default "en", then update all routes (profil.tsx, __root.tsx, event.$slug.tsx,
feed.tsx, dashboard.tsx, reset-password.tsx, source.$sourceId.tsx,
unsubscribe.tsx) to import and use getLocaleFromMatches instead of duplicating
the logic.

In `@apps/web/src/routes/reset-password.tsx`:
- Around line 20-32: Extract the duplicated locale-resolution logic used inside
head callbacks into a shared helper (e.g., create getLocaleFromMatches(matches)
in your i18n library like "@/lib/i18n/..."); update each head function (the
head: ({ matches }) => { ... } blocks in reset-password.tsx and the other pages:
salvate.tsx, unsubscribe.tsx, source.$sourceId.tsx, activitate.tsx,
dashboard.tsx, feed.tsx) to call getLocaleFromMatches(matches) and then pass its
return value to getString(locale, "reset.metaTitle") (or the page's meta key);
ensure getLocaleFromMatches encapsulates the current checks (matches[0]?.context
is object, "locale" in context, allowed locale values ["ro","en"], default "en")
so future locale changes are centralized.
- Around line 41-49: resetPasswordSchema is recreated on every render causing
unnecessary allocations and stale validator messages when the locale changes;
wrap the Zod schema creation in useMemo so its reference is stable and updates
only when the translation function t changes. Import useMemo from React and
replace the current const resetPasswordSchema = ... with const
resetPasswordSchema = useMemo(() => z.object({...}).refine(...), [t]); this
keeps useForm's validator reference stable while allowing error messages to
update when t changes.

In `@apps/web/src/routes/salvate.tsx`:
- Around line 56-71: The inline SVG elements used as decorative icons (the <svg>
with className "size-8" inside the rounded div and the other decorative SVG
elsewhere in this component) are not marked as decorative for assistive tech;
add aria-hidden="true" and focusable="false" to both SVG tags so screen readers
ignore them and they aren’t keyboard-focusable. Locate the SVG elements
(className "size-8") in the component JSX and add those attributes to each
decorative SVG.
- Around line 17-20: The function safePositiveInt currently allows 0
(Math.max(0,...)) which conflicts with the expected behaviour in /feed and the
semantics implied by the name; change safePositiveInt to enforce a minimum of 1
(use Math.max(1, Math.floor(n))) so callers like EventCard and
runtimeConfig.eventCardMaxSources get at least one source; keep the existing
fallback behavior for non-finite inputs and update any tests or callers if they
depend on 0 semantics.

In `@apps/web/src/routes/source`.$sourceId.tsx:
- Around line 54-63: The code repeats loaderData?.source.name checks; compute a
single sourceName variable from loaderData?.source?.name once (e.g., const
sourceName = loaderData?.source?.name) and then use that same symbol in title
and description: set title to sourceName ? `${sourceName} — ${SITE.name}` :
getString(locale, "source.metaTitle"), and for description use getString(locale,
"source.metaDescriptionLoaded").replace("{name}", sourceName ??
getString(locale, "source.metaTitle")) (or fall back to getString(locale,
"source.metaDescription") when loaderData is missing) so title, description, and
the name replacement all use the same sourceName value consistently.
- Around line 219-222: The translations reuse the `{count}` placeholder for
non-count numeric values (reliabilityScore, rollingBiasMean, rollingBiasStddev);
update the translation keys (e.g., change
source.reliability/source.mean/source.stddev to use `{score}` or `{value}`
instead of `{count}`) and then update the JSX replace calls to use the new
placeholder names (replace("{score}", String(source.reliabilityScore)) or
replace("{value}", String(...))) for the elements currently calling
t("source.reliability") and the rolling bias mean/stddev code paths (the
.replace(...) calls around the reliabilityScore, rollingBiasMean,
rollingBiasStddev usages), ensuring all occurrences (including the block around
lines 317–332) are updated consistently.

In `@apps/web/src/routes/unsubscribe.tsx`:
- Around line 137-142: The fallback `email ? email : ""` is redundant because
the early return when `!email` guarantees `email` is truthy; update the success
message generation to call t("unsubscribe.successBody").replace("{email}",
email) directly, removing the ternary and dead-code fallback so the component
uses the existing `email` variable without unnecessary checks.

---

Outside diff comments:
In `@apps/web/src/components/share-event-button.tsx`:
- Line 21: The component currently destructures and ignores the summary prop
(exported on ShareEventButtonProps) and calls navigator.share with duplicate
values (text: title), so either restore summary into the shared text or remove
the prop entirely; update the ShareEventButton component by removing the dead
"_summary" destructure and use the actual summary prop when building the share
payload (navigator.share({ title, text: summary ?? title, url })) so title and
text are not identical, and if you intend to drop summary instead, remove it
from ShareEventButtonProps and from callers like the code in event.$slug.tsx to
stop computing/passing it.

In `@apps/web/src/components/sign-in-form.tsx`:
- Around line 16-23: Replace the hardcoded redirectFallbackOrigin used by
getPasswordResetRedirectURL with the app origin from SITE.url: import SITE from
"@/lib/seo" (or destructure SITE.url) and when typeof window === "undefined"
return `${SITE.url}/reset-password` instead of using redirectFallbackOrigin;
remove or replace the redirectFallbackOrigin constant so server-side rendered
password-reset links use SITE.url consistently with the codebase.

In `@apps/web/src/components/ui/page-loading-state.tsx`:
- Around line 7-8: The hardcoded English defaults for the component props title
and description prevent the i18n fallbacks from ever running; remove those
default parameter values (or set them to undefined) in the PageLoadingState
component signature so that the nullish coalescing used later (the title ?? ...
and description ?? ... expressions) can trigger the translated fallbacks; update
the component’s props (title, description) to be optional and rely on the
existing ?? translations instead of supplying English literals as defaults.

In `@apps/web/src/components/user-menu.tsx`:
- Around line 21-29: The handleSignOut function lacks error handling; wrap the
await authClient.signOut(...) call in a try/catch inside handleSignOut, keep the
existing fetchOptions.onSuccess to call location.reload() on success, and in the
catch block surface a user-facing error (e.g., dispatch a toast/snackbar or set
an error state) and log the error (console.error or existing logger) so
network/server failures give feedback and can be diagnosed; reference
handleSignOut and authClient.signOut when making this change.

In `@apps/web/src/routes/reset-password.tsx`:
- Around line 152-156: The per-field error messages rendered from
field.state.meta.errors are not announced to screen readers; update the error
markup (the <p> elements mapped from field.state.meta.errors) to be an ARIA live
region — e.g., add role="alert" (or wrap the field error group with
aria-live="polite") so assistive tech announces validation failures, and also
ensure the corresponding input elements in the same field group include an
accessible label (aria-label or linked <label>) to satisfy the form guidelines;
locate the error renderings around the field.state.meta.errors map and the
associated input components to make these changes.

In `@apps/web/src/routes/unsubscribe.tsx`:
- Around line 53-58: The useEffect closure references unsubscribe.reset but does
not include unsubscribe in the dependency array, which can trigger
exhaustive-deps lint; update the effect to either add unsubscribe to the deps or
destructure the stable callback (e.g., const { reset } = unsubscribe) and depend
on that reset instead—modify the effect that uses useEffect, unsubscribe.reset,
setLastEmail, email, and lastEmail so the dependency array includes the chosen
stable reference (unsubscribe or reset) to satisfy the linter and ensure correct
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c82940c2-1302-42a4-bd7d-bc5acf885257

📥 Commits

Reviewing files that changed from the base of the PR and between 85a5371 and ab7b69d.

📒 Files selected for processing (49)
  • apps/web/src/components/LanguagePicker.tsx
  • apps/web/src/components/SignInPrompt.tsx
  • apps/web/src/components/auth-prompt-banner.tsx
  • apps/web/src/components/auth-social.tsx
  • apps/web/src/components/bias-balance-meter.tsx
  • apps/web/src/components/bias-indicator.tsx
  • apps/web/src/components/bookmark-button.tsx
  • apps/web/src/components/feed/articles-list.tsx
  • apps/web/src/components/feed/event-card.tsx
  • apps/web/src/components/feed/event-claim-comparison.tsx
  • apps/web/src/components/feed/event.tsx
  • apps/web/src/components/feed/source-coverage-summary.tsx
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/Footer.tsx
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/components/profile/AnonymousProfile.tsx
  • apps/web/src/components/profile/AuthenticatedProfile.tsx
  • apps/web/src/components/share-event-button.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/sign-up-form.tsx
  • apps/web/src/components/streak-activity-calendar.tsx
  • apps/web/src/components/ui/page-loading-state.tsx
  • apps/web/src/components/user-menu.tsx
  • apps/web/src/lib/dates.ts
  • apps/web/src/lib/i18n/LocaleContext.tsx
  • apps/web/src/lib/i18n/getServerLocale.ts
  • apps/web/src/lib/i18n/resolveLocale.ts
  • apps/web/src/lib/i18n/strings.ts
  • apps/web/src/lib/seo.ts
  • apps/web/src/router.tsx
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/contact.tsx
  • apps/web/src/routes/cum-functioneaza.tsx
  • apps/web/src/routes/dashboard.tsx
  • apps/web/src/routes/despre.tsx
  • apps/web/src/routes/event.$slug.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/parteneri.tsx
  • apps/web/src/routes/politica-confidentialitate.tsx
  • apps/web/src/routes/profil.tsx
  • apps/web/src/routes/reset-password.tsx
  • apps/web/src/routes/salvate.tsx
  • apps/web/src/routes/source.$sourceId.tsx
  • apps/web/src/routes/sursele-noastre.tsx
  • apps/web/src/routes/termeni.tsx
  • apps/web/src/routes/unsubscribe.tsx
  • packages/backend/convex/schema.ts
  • packages/backend/convex/user.ts

Comment thread apps/web/src/components/feed/event-claim-comparison.tsx Outdated
Comment thread apps/web/src/components/feed/event-claim-comparison.tsx Outdated
Comment thread apps/web/src/components/layout/MobileTabBar.tsx Outdated
};

return (
<div className="bg-gradient-to-b from-background via-background to-muted/20">

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Which Tailwind v4 release fully removed the legacy bg-gradient-to-aliases in favor ofbg-linear-to-?

💡 Result:

Tailwind CSS v4.1 is the release that fully removed the legacy bg-gradient-to-* aliases in favor of bg-linear-to-*. In Tailwind CSS v4.0, Tailwind introduced the rename from bg-gradient-* (v3) to bg-linear-* (v4) and provided upgrade/template migrations to rewrite class names accordingly [1][2]. The Tailwind v4.1 release includes the period when these legacy aliases are no longer available, i.e., bg-gradient-to-* is removed and only bg-linear-to-* remains [3].

Citations:


🏁 Script executed:

# Verify AuthenticatedProfile.tsx line 54 contains bg-gradient-to-b
cat -n apps/web/src/components/profile/AuthenticatedProfile.tsx | sed -n '50,60p'

Repository: flvvius/news

Length of output: 580


🏁 Script executed:

# Check event.$slug.tsx for new-style gradient utilities (lines 243 and 263 as mentioned)
cat -n apps/web/src/routes/event.\$slug.tsx | sed -n '240,270p'

Repository: flvvius/news

Length of output: 1654


🏁 Script executed:

# Search for all instances of deprecated bg-gradient-to-* in the codebase
rg 'bg-gradient-to-' --type tsx --type jsx

Repository: flvvius/news

Length of output: 84


🏁 Script executed:

# Check Tailwind version in package.json
cat package.json | grep -A 2 -B 2 tailwindcss

Repository: flvvius/news

Length of output: 38


🏁 Script executed:

# Try searching for deprecated bg-gradient utilities with simpler rg command
rg 'bg-gradient-to-' --glob '*.tsx' --glob '*.jsx'

Repository: flvvius/news

Length of output: 1016


🏁 Script executed:

# Check package.json differently
head -50 package.json | grep -i tailwind

Repository: flvvius/news

Length of output: 38


🏁 Script executed:

# Search for Tailwind version in various config/package files
find . -maxdepth 3 -type f \( -name "package.json" -o -name "package-lock.json" -o -name "tailwind.config.*" \) -exec grep -l "tailwindcss\|version" {} \; | head -5

Repository: flvvius/news

Length of output: 170


🏁 Script executed:

# Check the actual tailwind version specified
rg '"tailwindcss"' --type json -A 1 -B 1

Repository: flvvius/news

Length of output: 383


Replace bg-gradient-to-b with bg-linear-to-b — Tailwind v4.1 removed legacy gradient class names.

Tailwind v4.0 renamed bg-gradient-to-* utilities to bg-linear-to-*, and v4.1 fully removed the legacy aliases. This project uses Tailwind v4.1.3, where bg-gradient-to-b does not exist and produces no background gradient. Other files in the codebase already use the correct naming (e.g., bg-linear-to-b in event.$slug.tsx), so this needs to match.

🛠️ Fix
-    <div className="bg-gradient-to-b from-background via-background to-muted/20">
+    <div className="bg-linear-to-b from-background via-background to-muted/20">
📝 Committable suggestion

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

Suggested change
<div className="bg-gradient-to-b from-background via-background to-muted/20">
<div className="bg-linear-to-b from-background via-background to-muted/20">
🤖 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 `@apps/web/src/components/profile/AuthenticatedProfile.tsx` at line 54, In the
AuthenticatedProfile component replace the deprecated Tailwind class name:
update the div whose className contains "bg-gradient-to-b from-background
via-background to-muted/20" to use "bg-linear-to-b" instead of
"bg-gradient-to-b" so it matches the Tailwind v4.1 naming (keep the other
classes unchanged).

Comment thread apps/web/src/components/profile/AuthenticatedProfile.tsx Outdated
Comment thread apps/web/src/routes/salvate.tsx
Comment thread apps/web/src/routes/salvate.tsx Outdated
Comment thread apps/web/src/routes/source.$sourceId.tsx Outdated
Comment thread apps/web/src/routes/source.$sourceId.tsx
Comment thread apps/web/src/routes/unsubscribe.tsx

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (4)
apps/web/src/components/ui/page-loading-state.tsx (1)

19-47: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider adding ARIA live region for loading announcements.

The loading state displays dynamic content but lacks live-region semantics. Screen readers may not announce the loading status reliably.

♿ Proposed enhancement
         <div className="flex flex-col gap-6">
-          <Card className="overflow-hidden border-border/70 bg-card/80 shadow-sm">
+          <Card 
+            className="overflow-hidden border-border/70 bg-card/80 shadow-sm"
+            role="status"
+            aria-live="polite"
+          >

As per coding guidelines, apps/web/**: - Focus on web performance and accessibility.

🤖 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 `@apps/web/src/components/ui/page-loading-state.tsx` around lines 19 - 47, Add
an ARIA live region to announce the loading state by wrapping the dynamic
loading text (e.g., the elements that render resolvedTitle/resolvedDescription
or the Loader2/ CardHeader area) in a container with role="status"
aria-live="polite" aria-atomic="true" (or add a visually-hidden <div> with those
attributes that outputs a concise message like "Loading {resolvedTitle}" or the
resolvedDescription), and consider setting aria-busy="true" on the outer
container while loading; update the JSX around CardHeader/CardContent to include
this live region so screen readers reliably receive loading announcements.
apps/web/src/components/sign-up-form.tsx (1)

119-178: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add localized aria-labels to the sign-up inputs.

The visible labels are good, but this repo’s form rule also requires an aria-label on each input. That currently affects the name, email, and password fields here.

As per coding guidelines, "Forms must have aria-label on inputs and ARIA live regions (aria-live, role) on status messages".

🤖 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 `@apps/web/src/components/sign-up-form.tsx` around lines 119 - 178, The name,
email and password Input components are missing aria-labels required by our form
rules; add an aria-label prop to each Input inside the form.Field for "name",
"email", and "password" (use the existing i18n keys so the label is localized,
e.g. t("auth.name"), t("auth.email"), t("auth.password") or the appropriate
translation keys used for visible labels/placeholders), keeping other props
(like disabled={emailLocked}) unchanged so the accessible label matches the
visible label.
apps/web/src/components/sign-in-form.tsx (1)

136-145: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add localized aria-labels to the sign-in inputs.

Both form fields still miss explicit aria-label props. The visible labels help, but this repository requires aria-label on inputs too.

As per coding guidelines, "Forms must have aria-label on inputs and ARIA live regions (aria-live, role) on status messages".

Also applies to: 163-245

🤖 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 `@apps/web/src/components/sign-in-form.tsx` around lines 136 - 145, The Input
elements (e.g., the email Input rendered with id/name from field.name and value
from field.state.value, and the corresponding password Input later) are missing
explicit aria-label attributes; update the Input components (the ones using
field.handleChange/field.handleBlur and Label) to include aria-label props using
the localized strings (e.g., t("auth.email") for the email field and
t("auth.password") for the password field) so each input has an accessible,
localized aria-label; apply the same change to the other form inputs in the
sign-in form (the block around lines 163-245) to ensure all inputs include
aria-labels per the accessibility guideline.
apps/web/src/routes/feed.tsx (1)

181-188: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add aria-label to the icon-only close button.

The sr-only text helps, but this still misses the explicit aria-label required by the repo’s accessibility rule for icon-only buttons.

♿ Suggested fix
                 <DrawerClose asChild>
                   <Button
                     variant="ghost"
                     size="icon"
                     className="size-8 rounded-full"
+                    aria-label={t("feed.close")}
                   >
                     <XIcon className="size-4" />
                     <span className="sr-only">{t("feed.close")}</span>
                   </Button>
                 </DrawerClose>

As per coding guidelines, "Buttons must have visible text or aria-label for accessibility".

🤖 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 `@apps/web/src/routes/feed.tsx` around lines 181 - 188, The icon-only close
Button in feed.tsx (the Button wrapping XIcon) lacks an explicit aria-label; add
an aria-label prop to that Button (e.g., aria-label={t("feed.close")}) so it
satisfies the repo accessibility rule for icon-only buttons while keeping the
existing XIcon and sr-only span intact; update the Button JSX (the Button
component that currently has variant="ghost" size="icon" className="size-8
rounded-full") to include the aria-label prop.
♻️ Duplicate comments (1)
apps/web/src/lib/i18n/resolveLocale.ts (1)

31-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip q=0 languages during Accept-Language resolution.

q=0 explicitly means “not acceptable”, but the current filter/sort path still leaves those entries in weightedCodes. A header like ro;q=0,en;q=0 will currently resolve to a supported locale instead of falling through to the default.

Suggested fix
   const weightedCodes = header
     .split(",")
     .map((part) => {
       const [languagePart, ...params] = part.split(";");
       const code = languagePart?.trim().toLowerCase().split("-")[0];
@@
       return {
         code,
         weight: Number.isFinite(weight) ? weight : 1,
       };
     })
-    .filter((entry): entry is { code: string; weight: number } => Boolean(entry.code))
+    .filter(
+      (entry): entry is { code: string; weight: number } =>
+        Boolean(entry.code) && entry.weight > 0,
+    )
     .sort((a, b) => b.weight - a.weight);
🤖 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 `@apps/web/src/lib/i18n/resolveLocale.ts` around lines 31 - 50, The
Accept-Language parsing builds weightedCodes but doesn’t remove entries with q=0
(explicitly unacceptable), so resolveLocale may consider them; update the
weightedCodes construction to filter out entries where weight === 0 (e.g., after
computing weight in the .map or via an additional .filter) so that the
subsequent loop over weightedCodes (the for...of that checks SUPPORTED) will
skip q=0 languages and fall back to the default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/layout/MobileTabBar.tsx`:
- Around line 65-68: The bottom padding inline style currently overrides the
Tailwind "pb-4" causing the bar to sit flush on devices without safe-area
insets; update the MobileTabBar component's style prop (the element using
style={{ paddingBottom: "env(safe-area-inset-bottom)" }}) to preserve the base
gap by folding the fixed spacing into the inline value—use a CSS calc that adds
the fallback 1rem (or equivalent of pb-4) to env(safe-area-inset-bottom) with a
safe fallback for env when missing so the effective padding keeps the pb-4 gap
plus any safe-area inset.

In `@apps/web/src/routes/__root.tsx`:
- Around line 123-125: The call to decodeURIComponent(cookieLocale) in the
beforeLoad flow can throw a URIError for malformed bv_locale values; wrap the
decoding in a safe guard (e.g., try/catch) so that if
decodeURIComponent(cookieLocale) throws you catch the error and set
decodedCookieLocale to null (or a safe default) instead of allowing the
exception to bubble and break client navigation; update the logic around
decodedCookieLocale (and any callers) to use this safely-decoded value.

In `@apps/web/src/routes/activitate.tsx`:
- Around line 129-146: The admin config editor is currently gated on
topicDiagnostics (currentSettings = topicDiagnostics?.[0]?.settings) so admins
cannot edit settings in fresh environments; add a dedicated config read query
(e.g., useQuery(api.config.get) or similar) and use its result as the source of
truth instead of topicDiagnostics, update the useEffect that seeds
minScoreInput/confidenceRatioInput/maxTopicsInput to read from that config query
(or fall back to sensible defaults) and remove the UI render gating that checks
topicDiagnostics.length > 0 so the admin section always renders; keep using
setConfig (useConvexMutationHook(api.config.set)) for saves.
- Around line 660-698: The three admin config Input components (ids
topic-inference-min-score, topic-inference-confidence-ratio,
topic-inference-max-topics) are missing aria-labels; add localized aria-label
props to each Input using the same i18n keys as their visible labels (e.g.
t("activity.admin.minScore") for the min score input,
t("activity.admin.confidence") for the confidence ratio input, and
t("activity.admin.maxTopics") for the max topics input) so the inputs remain
functionally identical (keep value, onChange, disabled) but now include
accessible aria-label attributes.
- Around line 200-220: The current UI calls setConfig three times in parallel
(setConfig for keys topic_inference_min_score, topic_inference_confidence_ratio,
topic_inference_max_topics) which can leave configs partially applied if one
write fails; implement a single Convex mutation named setTopicInferenceSettings
that accepts the three values and updates all three keys inside one server-side
transaction, then replace the Promise.all([...setConfig(...)]) call with a
single call to the new setTopicInferenceSettings mutation so the three settings
are applied atomically.

In `@apps/web/src/routes/dashboard.tsx`:
- Around line 53-57: The useEffect that redirects authenticated users always
sends them to "/activitate" and ignores any validated redirect from the
route/search params; update the effect (the useEffect that checks currentUser
and calls navigate) to read the validated redirect (from the route loader,
search params, or your existing helper — e.g., a validatedRedirect variable
provided by the route or a getValidatedRedirect() helper) and call navigate({
to: validatedRedirect || "/activitate", replace: true }) instead of hardcoding
"/activitate", keeping the same dependency array [currentUser, navigate].

In `@apps/web/src/routes/feed.tsx`:
- Line 89: The CommandInput in feed.tsx currently only uses a placeholder so
screen readers lose the accessible name once a value is entered; update the
<CommandInput> instance to include an explicit aria-label (e.g.,
aria-label={t("feed.topic.search")}) or a prop that sets the accessible name so
the input keeps a reliable label when populated, and verify any related status
messages use appropriate ARIA live regions (aria-live/role) per the
form-labeling guideline.

In `@packages/backend/convex/clustering.ts`:
- Around line 5316-5331: The fallback path is forcing the local attach to
downgrade to heuristics by calling tryBatchLocalAttach(..., true), which ignores
embedding similarity; change those calls to preserve local embedding matching
(remove or set the forceFallback boolean to false) so tryBatchLocalAttach uses
embeddings and only falls back to findHeuristicCandidate when necessary; update
the calls that run after hydrating candidateCache from
getRecentClusterCandidates (and the other occurrences where tryBatchLocalAttach
is invoked in this file) to call tryBatchLocalAttach without the
forced-heuristic flag, ensuring hydrateClusterCandidate and candidateCache
entries are still used for embedding-based matching.
- Around line 2396-2435: getChangedClusterCandidates currently filters by
updatedAt but paginates by lastArticleAt, which is not a stable incremental
cursor; change the query to use a stable composite cursor/index on (updatedAt,
_creationTime) instead of lastArticleAt. Specifically, update the eventCandidacy
queries in getChangedClusterCandidates to use an index that orders by updatedAt
then _creationTime (or add one if missing), apply the since cursor as a
composite (updatedAt, _creationTime) filter and sort by updatedAt desc then
_creationTime desc, and use that same composite tie-breaker when
slicing/advancing lastProcessedAt so no rows are skipped; remove the mixing of
lastArticleAt-based pagination and the limit*3 heuristic. Ensure filtering for
singletonOnly and status remains the same but rely on the composite cursor for
stable pagination.
- Around line 276-306: The current check-then-record race must be replaced with
an atomic reserve/consume pattern: create and call a Convex mutation (e.g.,
internal.vectorSearchBudget.reserveUsage) from the search path (before executing
a batch of vector searches or per-search) that atomically checks budget and
reserves the QGB/slots, and then decrement/commit that reservation via another
mutation (e.g., internal.vectorSearchBudget.consumeReservation) or let it expire
if not used; update the callers that currently call checkBudget and only later
call flushJobMetrics (referencing flushJobMetrics, checkBudget, and the
vectorSearchBudget mutations) to use reserveUsage before running searches and to
consume or release the reservation in finally/exception handlers so mid-run
exceptions cannot drop already-reserved usage; apply the same pattern to the
other spots noted around lines 309-313.

In `@packages/backend/convex/schema.ts`:
- Around line 123-133: The change to the vector index dimensions for
eventEmbeddings.by_embedding from 1536 to 512 requires a migration/backfill:
either re-embed all existing eventEmbeddings to 512-d vectors or delete them
before deploying. Update EMBEDDING_VERSION in enrichmentNode.ts to trigger
reprocessing (or write a deletion job), and implement an idempotent backfill job
using the migrations.ts pattern to iterate existing eventEmbeddings,
compute/store new 512-d embeddings (or remove rows) and mark progress so the job
can be resumed; ensure the migration runs and completes prior to shipping the
schema change to avoid mismatched vector lengths causing search failures.

In `@packages/backend/convex/seeds.ts`:
- Around line 147-169: The seeded rows call new Date().toISOString().slice(0,10)
multiple times which can produce inconsistent day buckets across fields or rows;
compute a single day string once (e.g., const day = new
Date().toISOString().slice(0,10)) before creating the two event embedding
inserts and reuse that variable for updatedDayBucket, mergeSearchBucket and
singletonSearchBucket in both ctx.db.insert calls (located where dummyEmbedding
and the two ctx.db.insert("eventEmbeddings", { ... }) calls are defined) so each
row and all its bucket fields share the same logical timestamp.

In `@packages/backend/convex/vectorSearchBudget.ts`:
- Around line 221-274: recordUsage currently always increments daily totals via
adjustDailyUsage and inserts a vectorSearchRuns row, so retries double-count;
make it idempotent by runId: first query for an existing run by args.runId (use
ctx.db.get / ctx.db.query on "vectorSearchRuns" keyed by runId or a unique
index), and if found return {recorded: true} without calling adjustDailyUsage or
inserting; otherwise proceed to call adjustDailyUsage and
ctx.db.insert("vectorSearchRuns") as now. Ensure the existence check happens
before calling adjustDailyUsage and that you use the same runId field used in
ctx.db.insert to prevent races (or perform both operations in a single DB
transaction if supported).

---

Outside diff comments:
In `@apps/web/src/components/sign-in-form.tsx`:
- Around line 136-145: The Input elements (e.g., the email Input rendered with
id/name from field.name and value from field.state.value, and the corresponding
password Input later) are missing explicit aria-label attributes; update the
Input components (the ones using field.handleChange/field.handleBlur and Label)
to include aria-label props using the localized strings (e.g., t("auth.email")
for the email field and t("auth.password") for the password field) so each input
has an accessible, localized aria-label; apply the same change to the other form
inputs in the sign-in form (the block around lines 163-245) to ensure all inputs
include aria-labels per the accessibility guideline.

In `@apps/web/src/components/sign-up-form.tsx`:
- Around line 119-178: The name, email and password Input components are missing
aria-labels required by our form rules; add an aria-label prop to each Input
inside the form.Field for "name", "email", and "password" (use the existing i18n
keys so the label is localized, e.g. t("auth.name"), t("auth.email"),
t("auth.password") or the appropriate translation keys used for visible
labels/placeholders), keeping other props (like disabled={emailLocked})
unchanged so the accessible label matches the visible label.

In `@apps/web/src/components/ui/page-loading-state.tsx`:
- Around line 19-47: Add an ARIA live region to announce the loading state by
wrapping the dynamic loading text (e.g., the elements that render
resolvedTitle/resolvedDescription or the Loader2/ CardHeader area) in a
container with role="status" aria-live="polite" aria-atomic="true" (or add a
visually-hidden <div> with those attributes that outputs a concise message like
"Loading {resolvedTitle}" or the resolvedDescription), and consider setting
aria-busy="true" on the outer container while loading; update the JSX around
CardHeader/CardContent to include this live region so screen readers reliably
receive loading announcements.

In `@apps/web/src/routes/feed.tsx`:
- Around line 181-188: The icon-only close Button in feed.tsx (the Button
wrapping XIcon) lacks an explicit aria-label; add an aria-label prop to that
Button (e.g., aria-label={t("feed.close")}) so it satisfies the repo
accessibility rule for icon-only buttons while keeping the existing XIcon and
sr-only span intact; update the Button JSX (the Button component that currently
has variant="ghost" size="icon" className="size-8 rounded-full") to include the
aria-label prop.

---

Duplicate comments:
In `@apps/web/src/lib/i18n/resolveLocale.ts`:
- Around line 31-50: The Accept-Language parsing builds weightedCodes but
doesn’t remove entries with q=0 (explicitly unacceptable), so resolveLocale may
consider them; update the weightedCodes construction to filter out entries where
weight === 0 (e.g., after computing weight in the .map or via an additional
.filter) so that the subsequent loop over weightedCodes (the for...of that
checks SUPPORTED) will skip q=0 languages and fall back to the default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 61b2359d-21c3-4c07-8f08-87ae03805e27

📥 Commits

Reviewing files that changed from the base of the PR and between ab7b69d and 389fe3e.

⛔ Files ignored due to path filters (1)
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**, !**/_generated/**
📒 Files selected for processing (32)
  • apps/web/src/components/LanguagePicker.tsx
  • apps/web/src/components/feed/event-claim-comparison.tsx
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/components/profile/AnonymousProfile.tsx
  • apps/web/src/components/profile/AuthenticatedProfile.tsx
  • apps/web/src/components/share-event-button.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/sign-up-form.tsx
  • apps/web/src/components/ui/page-loading-state.tsx
  • apps/web/src/components/user-menu.tsx
  • apps/web/src/hooks/use-scroll-visibility.ts
  • apps/web/src/lib/i18n/LocaleContext.tsx
  • apps/web/src/lib/i18n/getLocaleFromMatches.ts
  • apps/web/src/lib/i18n/resolveLocale.ts
  • apps/web/src/lib/i18n/strings.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/dashboard.tsx
  • apps/web/src/routes/event.$slug.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/profil.tsx
  • apps/web/src/routes/reset-password.tsx
  • apps/web/src/routes/salvate.tsx
  • apps/web/src/routes/source.$sourceId.tsx
  • apps/web/src/routes/unsubscribe.tsx
  • packages/backend/convex/clustering.ts
  • packages/backend/convex/config.ts
  • packages/backend/convex/ingestion.ts
  • packages/backend/convex/schema.ts
  • packages/backend/convex/seeds.ts
  • packages/backend/convex/vectorSearchBudget.ts

Comment thread apps/web/src/components/layout/MobileTabBar.tsx Outdated
Comment thread apps/web/src/routes/__root.tsx Outdated
Comment thread apps/web/src/routes/activitate.tsx Outdated
Comment thread apps/web/src/routes/activitate.tsx Outdated
Comment thread apps/web/src/routes/activitate.tsx Outdated
Comment thread packages/backend/convex/clustering.ts
Comment thread packages/backend/convex/clustering.ts
Comment thread packages/backend/convex/schema.ts
Comment thread packages/backend/convex/seeds.ts Outdated
Comment thread packages/backend/convex/vectorSearchBudget.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 17

♻️ Duplicate comments (1)
packages/backend/convex/clustering.ts (1)

2447-2492: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cursor still advances past unprocessed rows when more than limit match.

The query now orders by (updatedAt, _creationTime) (good — that addresses the previous tie-breaker concern), but it still does take(limit) on a DESC-ordered set and then advanceChangedCandidateCursor (called at lines 4839-4843 and 5210-5214) moves lastProcessedAt to the maximum updatedAt of the returned rows. If more than limit candidacies satisfy updatedAt > sinceTs, the rows in (sinceTs, min(returned.updatedAt)) are silently dropped: they weren't returned, but the next run's sinceTs is now max(returned.updatedAt), so they will never be revisited. This reintroduces the same "callers will advance lastProcessedAt past them" failure mode flagged previously. Either order ASC and advance to the highest processed row, or only advance the cursor when the page is fully drained (e.g. rows.length < limit).

As per coding guidelines, "Review database schema changes carefully" and "Verify proper error handling".

🤖 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 `@packages/backend/convex/clustering.ts` around lines 2447 - 2492,
getChangedClusterCandidates currently queries two DESC-ordered sets with
.take(limit) and returns the top N, but advancing the cursor (via
advanceChangedCandidateCursor) to the max returned updatedAt will skip any
unreturned rows with updatedAt between sinceTs and that max. Fix by either
querying in ASC order so you can safely advance the cursor to the highest
processed row without skipping (change ordering to asc on
updatedAt/_creationTime and paginate forward), or keep DESC but only advance
lastProcessedAt when the page is fully drained (i.e., returnedRows.length <
limit) so unprocessed rows aren’t skipped; update getChangedClusterCandidates
and the cursor-advance callers (advanceChangedCandidateCursor) accordingly to
implement one of these strategies consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/layout/MobileTabBar.tsx`:
- Around line 27-31: The active-state boolean (isActive) is being reused to
decide whether to prevent navigation in handleTabClick, causing taps on the Feed
tab from /event/* to merely scroll instead of navigate back to /feed; update the
logic to compute two booleans: keep the existing isActive predicate (pathname
=== "/feed" || pathname.startsWith("/feed/") || pathname.startsWith("/event/"))
for visual highlighting, but introduce an isSameDestination check (e.g.,
pathname === to) and only call preventDefault/avoid navigation when
isSameDestination is true; update handleTabClick to use isSameDestination
instead of isActive so tapping from an event page navigates to /feed while still
showing the active visual state on /event/*.

In `@apps/web/src/components/sign-in-form.tsx`:
- Around line 24-50: getLocalizedSignInError is brittle because it relies on
exact English message strings; change it to normalize and check only the error
code against a small allowlist (e.g. "invalid_credentials", "invalid_password",
"invalid_login", case-insensitive, normalize underscores/hyphens) and return
t("auth.invalidCredentials") when the normalized code matches any allowlisted
value; keep the message/statusText as a best-effort fallback (return
error.error?.message ?? error.error?.statusText ?? t("auth.signInError")), and
ensure you trim/lowercase the code before comparing so variants like
"INVALID_CREDENTIALS" still match.
- Around line 17-22: The getPasswordResetRedirectURL function currently
concatenates paths with SITE.url which can break if SITE.url gains a trailing
slash; replace the string concatenation in getPasswordResetRedirectURL by
constructing the redirect using the URL API (e.g. new URL("/reset-password",
SITE.url).toString()) and do the same when using window.location.origin (or new
URL("/reset-password", window.location.origin).toString()) to ensure robust URL
joining; also scan the codebase for other occurrences of `${SITE.url}/...` and
switch them to new URL(...) or add explicit assertions/validation for SITE.url's
shape in the seo module.

In `@apps/web/src/components/sign-up-form.tsx`:
- Around line 78-81: The onError handler in the SignUpForm component currently
shows raw server text (error.error.message) which is English; replace that by
mapping server error codes/messages to i18n keys and passing the translated
string to toast.error. Reuse the existing getLocalizedSignInError logic (or
create getLocalizedSignUpError) to map common sign-up cases like "user already
exists" / "email already in use" to a translation key (e.g.,
t("auth.emailInUse")), then call that mapping from the onError in sign-up
(instead of error.error.message) and fall back to t("auth.unexpectedError") if
unmapped.
- Around line 85-91: The validation schema passed into the validators/onSubmit
object is created once and closes over t(...) so it doesn't update when locale
changes; wrap the z.object schema (the object with name/email/password using
t("auth.*")) in a useMemo keyed on the current locale (or i18n.language) and
pass that memoized schema into validators.onSubmit so validation messages update
immediately when the language changes.

In `@apps/web/src/components/ui/page-loading-state.tsx`:
- Around line 15-17: The default i18n keys are inconsistent: resolvedTitle falls
back to "feed.loading" while resolvedDescription uses "activity.loading.body";
update the defaults used in PageLoadingState so they come from a shared
namespace (for example use t("common.loading.title") for resolvedTitle and
t("common.loading.body") for resolvedDescription) so both defaults are symmetric
and callers can still override via the title and description props; adjust the
references to resolvedTitle and resolvedDescription in the component
accordingly.

In `@apps/web/src/routes/activitate.tsx`:
- Around line 168-191: currentSettings is rebuilt each render which is
misleading; either memoize it with useMemo or extract the three primitive values
directly so the useEffect deps are clearly stable. Replace the inline object
creation with a useMemo(() => ({ minScore: getNumericConfigValue(...),
confidenceRatio: ..., maxTopics: ... }), [minScoreConfig, confidenceRatioConfig,
maxTopicsConfig]) or assign const minScore = getNumericConfigValue(...), const
confidenceRatio = ..., const maxTopics = ... and then update the useEffect to
depend on those primitives and call setMinScoreInput, setConfidenceRatioInput,
and setMaxTopicsInput accordingly (referencing currentSettings, useEffect,
setMinScoreInput, setConfidenceRatioInput, setMaxTopicsInput,
getNumericConfigValue, TOPIC_INFERENCE_DEFAULTS).

In `@apps/web/src/routes/feed.tsx`:
- Around line 653-660: The status paragraph for indexed search (the JSX block
guarded by isSearching that renders t("feed.search.indexed").replace("{query}",
debouncedSearch)) must be an ARIA live region so screen readers are notified;
update that <p> to include role="status" aria-live="polite" and
aria-atomic="true" (preserving the existing className and text interpolation) so
the change is announced politely when isSearching becomes true.

In `@packages/backend/convex/clustering.ts`:
- Around line 276-364: The five helpers flushJobMetrics,
getVectorSearchBudgetState, reserveVectorSearch, consumeVectorSearchReservation,
and releaseVectorSearchReservation currently use ctx: any (and the last two use
reservationId: any); import ActionCtx from "./_generated/server" and replace
ctx: any with ctx: ActionCtx in all five function signatures, and change
reservationId: any to reservationId: Id<"vectorSearchReservations"> (import Id
from the same generated module if needed) for consumeVectorSearchReservation and
releaseVectorSearchReservation so Convex action context and reservation id types
are enforced.

In `@packages/backend/convex/config.ts`:
- Around line 868-874: The current forcedDefaultKeys array contains the
runtime-tunable keys (vector_search_daily_budget_qgb,
vector_search_budget_enabled, vector_search_fallback_mode_enabled,
clustering_vector_search_limit, merge_vector_search_limit,
recluster_vector_search_limit) which causes seedDefaults to overwrite admin-set
values; remove those vector_search_* keys (and the related
clustering/merge/recluster limits if they should be admin-tunable) from
forcedDefaultKeys so seedDefaults only inserts missing defaults and does not
patch existing rows, or conversely if you intend them to be immutable update
their descriptions and keep them in forcedDefaultKeys; edit the
forcedDefaultKeys array and validate seedDefaults (function seedDefaults)
behavior to ensure it does not call the patch/update path for keys meant to be
overridable.
- Around line 351-367: The server-side validation for args.minScore,
args.confidenceRatio, and args.maxTopics uses hardcoded ranges and must be
unified with the client to avoid drift; extract the bounds into a single shared
source (e.g., a new exported constants object like TOPIC_INFERENCE_BOUNDS or a
getTopicInferenceBounds query) and replace the literal checks in this validation
(the conditions that throw ConvexError for args.minScore, args.confidenceRatio,
args.maxTopics) with references to those shared bounds, then update the client
code in activitate.tsx to read the same shared constants/query instead of
duplicating the min/max literals so both layers use the identical values.

In `@packages/backend/convex/migrations.ts`:
- Around line 558-568: The rescheduling uses ctx.scheduler.runAfter(0, ...)
which queues immediate back-to-back runs and can overload partitions; change the
reschedule to use a small non-zero delay (e.g., 250–1000 ms) instead of 0 and
add an optional maxPages/remainingPages cap passed through the auto-continuation
payload so a single job cannot loop indefinitely; update the continuation logic
around scheduledContinuation/nextCursor to pass { cursor: nextCursor, pageSize:
safePageSize, autoContinue: true, remainingPages: <defaultOrArg> } and use
remainingPages-- on each reschedule, and apply the same change to the
queueEventShareAssetsBackfill rescheduling path and any callers of
api.migrations.deleteInvalidEventEmbeddingsFor512dVectorIndex to ensure both the
delay and page cap are respected.
- Around line 525-579: The migration deletes eventEmbeddings rows with wrong
dimensions but never requests re-embedding, leaving events invisible to vector
search; update deleteInvalidEventEmbeddingsFor512dVectorIndex to also queue each
affected event for re-embedding (or schedule a backfill job) immediately after
deletion: for every row you delete (in the loop where you inspect row.embedding
and call ctx.db.delete(row._id)), record the corresponding event id and either
call/schedule the existing re-embedding entrypoint (e.g., invoke the
ingestion/updateEventEmbedding flow or a new api.migrations.reembedEvent job)
via ctx.scheduler.runAfter or push it to your re-embedding queue so missing
embeddings are regenerated; ensure the scheduled task name and the parameter
(event id) match the handler you implement so deleted embeddings are recovered
automatically.

In `@packages/backend/convex/schema.ts`:
- Around line 710-727: The vectorSearchRuns table lacks a retention/TTL sweep
and will grow indefinitely; add a retention strategy by (1) adding either an
expiresAt numeric column or relying on the existing createdAt plus a new index
that supports age-based pagination (e.g., add index "by_createdAt" on
["createdAt"] or keep "by_job_createdAt"), and (2) implement a scheduled cleanup
mutation (e.g., sweepVectorSearchRuns or cleanupVectorSearchRuns) that paginates
using the chosen index (use query(...).index("by_job_createdAt") or the new
"by_createdAt") and deletes rows with createdAt < now - RETENTION_DAYS*86400;
make retention configurable (RETENTION_DAYS/default 30) and schedule the
mutation via your existing cron/scheduler wiring so old rows are deleted in
pages.

In `@packages/backend/convex/vectorSearchBudget.ts`:
- Around line 469-480: The handler currently computes pageSize from a
potentially fractional v.number() so take(pageSize) can receive a non-integer;
update the normalization to floor the value after clamping so pagination always
uses an integer. Specifically, after computing Math.max(1, Math.min(limit ?? 50,
200)) for pageSize in the async handler (the function handling args: { limit }),
apply Math.floor (or equivalent) before passing pageSize into
ctx.db.query(...).take(pageSize) so take always receives a whole number.
- Around line 174-183: The cleanup in releaseExpiredReservations stops after a
single .take(100) page; change releaseExpiredReservations to page through
results until the query returns zero rows, releasing each batch before
continuing so all expired "vectorSearchReservations" with status "reserved" and
expiresAt <= now are processed; use the existing
ctx.db.query("vectorSearchReservations").withIndex("by_status_expiresAt", ...)
call inside a loop (or use cursor/offset paging) to fetch successive pages,
apply the same release logic to each batch, and adjust totals.qgbRead for every
released reservation so the budget calculation (used by reserveUsage) reflects
all freed quota before returning.
- Around line 90-153: The read-then-insert race can create duplicate
vectorSearchDailyTotal or vectorSearchDaily rows under concurrency; change
ensureDailyTotal and the insert branch of adjustDailyUsage to perform an
insert-with-retry: attempt the insert, but catch a duplicate/unique-constraint
error and then re-query the indexed unique lookup (using
query("vectorSearchDailyTotal").withIndex("by_date", ...) and
query("vectorSearchDaily").withIndex("by_date_shard", ...)) to return the
existing document instead of assuming insert succeeded; apply the same pattern
for both ensureDailyTotal and the else branch in adjustDailyUsage so creation is
effectively serialized and duplicates are resolved by fetching the authoritative
row.

---

Duplicate comments:
In `@packages/backend/convex/clustering.ts`:
- Around line 2447-2492: getChangedClusterCandidates currently queries two
DESC-ordered sets with .take(limit) and returns the top N, but advancing the
cursor (via advanceChangedCandidateCursor) to the max returned updatedAt will
skip any unreturned rows with updatedAt between sinceTs and that max. Fix by
either querying in ASC order so you can safely advance the cursor to the highest
processed row without skipping (change ordering to asc on
updatedAt/_creationTime and paginate forward), or keep DESC but only advance
lastProcessedAt when the page is fully drained (i.e., returnedRows.length <
limit) so unprocessed rows aren’t skipped; update getChangedClusterCandidates
and the cursor-advance callers (advanceChangedCandidateCursor) accordingly to
implement one of these strategies consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b9139c98-2bd8-417a-88da-71b7416db17a

📥 Commits

Reviewing files that changed from the base of the PR and between 389fe3e and 9347793.

📒 Files selected for processing (15)
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/sign-up-form.tsx
  • apps/web/src/components/ui/page-loading-state.tsx
  • apps/web/src/lib/i18n/resolveLocale.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/dashboard.tsx
  • apps/web/src/routes/feed.tsx
  • packages/backend/convex/clustering.ts
  • packages/backend/convex/config.ts
  • packages/backend/convex/migrations.ts
  • packages/backend/convex/schema.ts
  • packages/backend/convex/seeds.ts
  • packages/backend/convex/vectorSearchBudget.ts

Comment thread apps/web/src/components/layout/MobileTabBar.tsx
Comment thread apps/web/src/components/sign-in-form.tsx
Comment thread apps/web/src/components/sign-in-form.tsx
Comment thread apps/web/src/components/sign-up-form.tsx
Comment thread apps/web/src/components/sign-up-form.tsx
Comment thread packages/backend/convex/migrations.ts
Comment on lines +710 to +727
vectorSearchRuns: defineTable({
jobName: v.string(),
runId: v.string(),
date: v.string(),
qgbRead: v.number(),
vectorSearches: v.number(),
vectorMatchesReturned: v.number(),
vectorMatchesHydrated: v.number(),
vectorMatchesDiscardedPostFetch: v.number(),
usedFallbackMode: v.boolean(),
budgetAllowed: v.boolean(),
elapsedMs: v.number(),
metricsJson: v.string(),
createdAt: v.number(),
})
.index("by_run_id", ["runId"])
.index("by_date", ["date"])
.index("by_job_createdAt", ["jobName", "createdAt"]),

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.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

vectorSearchRuns has no retention strategy and will grow unbounded.

Every clustering/merge/recluster run writes a row here with a non-trivial JSON metrics blob. The only indexes are by_run_id, by_date, and by_job_createdAt, none of which support efficient deletion by age, and there's no scheduled trimming or TTL mutation in this PR. Over weeks of operation this table will accumulate many rows and inflate read/storage cost for the same admin dashboards that consume it. Consider adding a scheduled cleanup mutation that paginates by by_job_createdAt (or by_date) and deletes rows older than a configurable retention window (e.g., 30 days), mirroring how aiBudgetReservations is swept via by_expiresAt.

🤖 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 `@packages/backend/convex/schema.ts` around lines 710 - 727, The
vectorSearchRuns table lacks a retention/TTL sweep and will grow indefinitely;
add a retention strategy by (1) adding either an expiresAt numeric column or
relying on the existing createdAt plus a new index that supports age-based
pagination (e.g., add index "by_createdAt" on ["createdAt"] or keep
"by_job_createdAt"), and (2) implement a scheduled cleanup mutation (e.g.,
sweepVectorSearchRuns or cleanupVectorSearchRuns) that paginates using the
chosen index (use query(...).index("by_job_createdAt") or the new
"by_createdAt") and deletes rows with createdAt < now - RETENTION_DAYS*86400;
make retention configurable (RETENTION_DAYS/default 30) and schedule the
mutation via your existing cron/scheduler wiring so old rows are deleted in
pages.

Comment thread packages/backend/convex/vectorSearchBudget.ts Outdated
Comment thread packages/backend/convex/vectorSearchBudget.ts Outdated
Comment thread packages/backend/convex/vectorSearchBudget.ts
@flvvius
flvvius merged commit e5e3561 into main May 11, 2026
3 of 4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 12, 2026
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.

1 participant