Romanian news - #39
Conversation
Add normalizeRomanianDiacritics (NFC + legacy cedilla s/t -> comma-below) applied to titles, snippets, and extracted body text before embedding, clustering, and LLM calls. Add foldDiacriticsToAscii for matching contexts (clustering token overlap, fingerprints, slugs) so Romanian text written with and without diacritics compares equal instead of being gutted by ASCII-only filters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swap the US/English curated feeds for the Romanian launch set: 8 tier-1 verified-direct feeds (Digi24, HotNews, G4Media, Recorder, Agerpres, ZF, RISE Project, Europa Libera) + 7 live-verified tier-2 feeds (Adevarul, Libertatea, Stirile ProTV, Antena 3, Gandul, Biziday, SpotMedia) — 15 feeds total for the staged ramp. Add feed quarantine to ingestAllFeeds: after 5 consecutive failures a feed is skipped with a 6h probe backoff, surfaced in run-log counters and gauges instead of silently retried forever. Romanian headline suffixes and ro-RO Accept-Language for RSS fetches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pole definitions in each camp's own self-adopted terms, neutral indigo/amber visual language, finalized UI copy, and an explicit single-axis scope (second economic axis deferred by design). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add canonical bias objects ({ axis: "reformist_suveranist", score: -5..+5 })
on sources (bias) and articles (aiBias), keeping baseBias/aiBiasScore as
the derived single-score mirrors the UI consumes so single-axis UI behavior
is unchanged. All writers (ingestion source creation, enrichment bias
scoring, MBFC update path) maintain both; backfillNamedAxisBias migration
covers existing rows with paginated auto-continue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add sourceReputation.ts — the authoritative manual source-metadata layer: 15 launch-feed outlets + Ethical Media Alliance whitelist (PressOne, Context, Snoop) + Veridica/Expert Forum low-reliability layer (ActiveNews, National, OrtodoxINFO, SolidNews, Realitatea Plus), each with a hand-assigned reformist-suveranist axis score, 1-10 reliability, and a one-line provenance note stored on the source row. feeds.ts now derives bias/reliability from the seed (fails fast at module load if a launch feed is unrated), ingestion stores provenance, and seeds:seedRomanianSources upserts all rows idempotently. The MBFC update path never overwrites provenance-marked manual scores. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete mbfc.ts (integration was cron-disabled dead code and MBFC barely covers Romania) and the disabled cron block. The manual reputation seed in sourceReputation.ts is now the single documented source-metadata path; README/SYSTEM-OVERVIEW updated and a backlog note covers any future automated refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the wink-nlp + wink-eng-lite-web-model dependency (English-only POS model, meaningless on Romanian). Entity extraction now uses a Unicode-aware capitalized-sequence matcher with Romanian name connectors and role prefixes, preserving diacritics and digit-bearing names (Digi24, Antena 3, G4Media). Also fixes a latent filter bug that rejected every all-lowercase multi-word entity, and makes the candidate normalizer Unicode-aware. Clustering's own token extractor is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Default claim_analysis_enabled and article_fact_extraction_enabled to false (code defaults + seeded config, forced onto existing deployments). atomicFacts and claim schema stay intact — pause, not delete. The getEventClaims query now returns null when the flag is off so web and native hide the claim section entirely, while [] still means enabled-but-pending. Summaries and perspectives render fully with the flag off; bias detection stays enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Abstract the LLM client behind provider routing (lib/modelRouting.ts): gemini-* model ids route through Gemini's OpenAI-compatible endpoint (GEMINI_API_KEY), everything else through OpenAI, sharing the same PostHog instrumentation, budget reservations, and aiUsage cost logging. Default chat model for all pipeline call types is gemini-3.1-flash-lite, read from config (event_summary_model, article_bias_detection_model, …) with DEFAULT_CHAT_MODEL as the single fallback — no hardcoded model ids remain in the pipeline. Embeddings stay on text-embedding-3-small. seedDefaults migrates rows still holding the stale gpt-5-nano default while preserving explicit operator overrides; Gemini traffic is labeled with its provider in PostHog; Gemini pricing added to the rate card. callOpenAI is renamed callLLM. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
perspectiveSummaries keys renamed center/left/right -> neutral/reformist/suveranist across schema, pipeline (summarization, clustering merges, previews, quiz, share assets), and both clients. Legacy keys stay schema-readable; normalizedPerspectives() falls back to them at every read/merge boundary so pre-migration rows keep rendering and are never dropped by merges or resummarized needlessly. backfillPerspectiveAxisKeys converts stored events + previews. BiasIndicator, bias-balance meters, coverage chips, and perspective tabs now display the neutral pole labels from docs/bias-axis-spec.md (Reformist / Neutru / Suveranist, "Cadrare reformistă/suveranistă") in both locales; biasBalance still renders as a single −100..+100 value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite the event summary and bias-scoring prompts fully in Romanian:
system instructions explicitly require Romanian output with correct
diacritics, define the reformist-suveranist axis in each camp's own terms
(non-evaluative), and present per-article framing labels in Romanian.
Summary output keys are now neutral/reformist/suveranist with Romanian
fallback strings; the bias scorer emits the named-axis object
({ axis: "reformist_suveranist", score }) enforced via strict JSON
schema. Paused fact-extraction and claim prompts get the Romanian-output
rule too.
Summary signatures carry a prompt version so legacy English summaries
regenerate, and requeueBiasScoringForAxisChange re-scores previously
analyzed articles whose stored scores predate the axis change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocedure Clustering thresholds were already config-driven; this adds the Romanian measurement side: Romanian stopwords for title-token overlap (ASCII-folded to match the diacritic folding from BIV-102), a clusteringTuning:sweepClusteringThresholds internal query that scores the hand-labeled clusterPairLabels set with the production join rule (strong-similarity override + title lexical gate) and reports false-merge/false-split precision/recall per threshold, and docs/clustering-romanian-tuning.md documenting the collect→label→sweep→ choose procedure and where to record the chosen threshold. Embedding model and time-window rule unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add news.google.com/rss?hl=ro&gl=RO&ceid=RO:ro as an optional discovery source behind google_news_overlay_enabled (default false). Wrapper links are resolved to canonical publisher URLs before dedup via the shared batchexecute resolver (extracted from articleExtraction into lib/googleNews.ts, runtime-neutral); publisher hosts are normalized like canonical-URL dedup so mobile/AMP hosts match. Only domains that resolve to an existing source row (curated feeds + reputation seed) are ingested — unknown domains are counted and skipped. Overlay inserts schedule enrichment and each run logs resolved/unresolved/unmatched counters to pipelineRunLogs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADR-001: managed Gemini at launch, no self-hosted generative LLM infra; self-hosting is a V2-only option gated on data residency or 100M+ tokens/month (CPU NER sidecar explicitly carved out). Backlog spec for the Romanian NER microservice (roner/RONEC, FastAPI or ONNX-in-JS) with explicit do-not-build triggers tied to BIV-501/BIV-701. Backlog spec for two-stage model escalation (Flash-Lite + 3.5 Flash for perspectives/bias) deferred until BIV-701 shows a gap, including the required summary-call split. All referenced from the roadmap; stale roadmap AI stack row updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add eval/romanian/: build-sample.ts assembles 50-100 real Romanian articles across topics from a read-only Convex export, and run-eval.ts runs the PRODUCTION summary prompt against the configured model (gemini-3.1-flash-lite default) and scores three metrics per event: language+schema (deterministic looksRomanian gate + strict JSON), summary faithfulness and named-entity accuracy (judge model, unsupported claims / entity errors stored verbatim for human spot-checks), and bias-score sanity (deterministic pole-support check vs the reputation seed). Batched with --limit/--offset for small live runs, --dry-run for cost estimates, results written per run with a documented pass/fail bar and baseline table in the README. looksRomanian lives in convex/lib/romanian.ts with unit tests and requires Romanian function words (diacritics alone don't pass, so English text quoting Romanian names is caught). eval/romanian is carved out of the eval/ gitignore; tsx + openai added as root dev deps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkthroughThis PR renames the bias/perspective axis to reformist/suveranist across backend prompts, clustering, migrations, and i18n; introduces feature flags to hide quiz and claim-analysis features; adds a light/dark theme system; redesigns mobile safe-area layout and feed/event UI into editorial rows; adds static Romanian footer pages with shared branding; overhauls Romanian feed ingestion with tiered source reputation, quarantine, and a Google News overlay; adds image format sniffing/verification; improves auth session handling and model routing; adds a reading-activity calendar; and adds scroll restoration plus Playwright/Vitest tooling. ChangesBias axis rename
Feature flags
Theme system
Mobile layout & feed redesign
Static pages & branding
Ingestion & topic catalog
Image pipeline
Auth & model routing
Activity dashboard
Test infra
Estimated code review effort: 5 (Critical) | ~150 minutes Sequence Diagram(s)sequenceDiagram
participant Router
participant SessionStorage
participant BrowserWindow
Router->>Router: onRendered(action)
alt action is PUSH
Router->>BrowserWindow: scrollTo(0,0)
else action is BACK/FORWARD/GO
Router->>SessionStorage: read stored scrollY
Router->>BrowserWindow: poll scrollHeight until reachable
Router->>BrowserWindow: scrollTo(storedY, instant)
end
BrowserWindow-->>Router: user interaction (wheel/touch/key)
Router->>Router: cancel pending re-apply
sequenceDiagram
participant Ingestion
participant SourceReputation
participant FeedsCatalog
participant GoogleNewsRSS
participant ArticlesTable
Ingestion->>FeedsCatalog: load ALL_FEEDS (tier 1-3)
FeedsCatalog->>SourceReputation: getSourceReputation(domain)
SourceReputation-->>FeedsCatalog: baseBias, reliabilityScore, provenance
Ingestion->>Ingestion: isFeedQuarantined(meta, now)
alt not quarantined
Ingestion->>ArticlesTable: ingest feed items
end
Ingestion->>GoogleNewsRSS: fetch RO overlay RSS
GoogleNewsRSS-->>Ingestion: items with wrapper URLs
Ingestion->>Ingestion: resolveGoogleNewsUrl(canonical)
Ingestion->>ArticlesTable: insert deduped matched-source items
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The pre-AI presentation path in clustering.ts still emitted hardcoded
English ('This cluster currently includes…', 'Sources in this event
include…', 'Coverage is still being assembled…'), which surfaced as
English suffixes inside Romanian neutral summaries for events below the
AI-summary threshold. Adds romanianCount() for correct Romanian plural
formatting (singular / plural / 'de' partitive for 20+).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source logos default to icons.duckduckgo.com (Clearbit's logo API is dead). The share renderer previously dropped anything served with an icon content type, losing logos on generated share cards. Now the bytes are sniffed instead of trusting Content-Type (favicon CDNs routinely mislabel PNG as image/x-icon), real ICO containers are decoded with decode-ico and re-encoded to PNG via sharp, and other non-resvg-safe formats (WebP/AVIF/BMP/SVG) are normalized to PNG, since resvg only rasterizes PNG/JPEG/GIF hrefs. Verified against all 23 seeded Romanian domains: 18 serve PNG (one mislabeled), 4 serve true ICO (incl. BMP-frame ones), 1 JPEG — all now embed and render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agerpres articles without a photo emit og:image pointing at an HTML photo-detail page (foto.agerpres.ro/foto/detaliu/<id>) and a literal "linkul pozei" twitter:image. The extractor stored that page URL as the article image and event presentation promoted it to the event's main photo, which rendered broken. Enrichment now collects image candidates in priority order (og, twitter, json-ld, inline, raw) and only stores the first one whose fetched leading bytes sniff as a renderable image; HTML pages and favicons are rejected, capped at 3 verification fetches per article. The imageSniff SVG heuristic no longer accepts HTML documents that inline an early <svg> icon (caught by codex review). migrations:clearHtmlPageImageUrls repairs already-stored broken articles/events; clearHtmlPageImageForEventSlug spot-fixes one event and was already run against dev for the reported event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ployments
Root cause (shared by both tickets, verified empirically): the Convex runtime
reports NODE_ENV=production on EVERY deployment, dev included, so
collectTrustedOrigins() never added http://localhost:3001 to Better Auth's
trustedOrigins and the dev deployment's SITE_URL pointed at the convex.site
URL instead of the local web app. Every cookie- or Sec-Fetch-bearing request
from localhost was rejected with 403 INVALID_ORIGIN: login surfaced it raw
("Invalid origin"), signup masked it as the generic "A apărut o eroare
neașteptată", and the Google button swallowed it entirely because
authClient.signIn.social resolves with { error } instead of throwing.
Fixes:
- auth.ts: isProductionDeployment() now honors an explicit DEPLOY_ENV env var
(set to "development" on dev deployments) before falling back to NODE_ENV;
collectTrustedOrigins() uses it instead of raw NODE_ENV. Both are now pure
functions over an env record so they are unit-testable.
- auth-social.tsx: GoogleSignInButton surfaces { error } results as a toast
instead of silently doing nothing (BIV-808).
- Dev deployment env updated: SITE_URL=http://localhost:3001 (per the
documented setup), DEPLOY_ENV=development. README + .env comments document
the requirement.
- New web vitest setup (apps/web/vitest.config.ts, pnpm test) with regression
tests for the Google button; backend regression tests prove localhost is
trusted under the real Convex dev env shape (NODE_ENV=production +
DEPLOY_ENV=development) and NOT trusted in production.
Verified end-to-end on localhost: email login reaches credential validation,
signup returns 200 and creates the account, social sign-in returns the Google
OAuth URL with redirect_uri=http://localhost:3001/api/auth/callback/google
(accepted by the Google OAuth app, no redirect_uri_mismatch).
Codex review: flagged vi.mock factories closing over top-level fns; fixed via
vi.hoisted. Backend suite 126 pass, web suite 3 pass, tsc clean on both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s is paused Investigated in the ticket's order: the tab param plumbing was correct (UI feedSort → sort arg → distinct by_trending_score / by_last_updated_at index paths), and the suspected cause (no interaction data in dev) was wrong — trending never ranked on interactions. The real cause: computeTrendingScore weighted only factualSourceCount/factualArticleCount, which are produced by the claim-analysis pipeline that BIV-602 paused behind a feature flag. With those at 0/undefined the score reduced to the recency term (hours since epoch), an ordering identical to Latest. Reproduced on the dev deployment: first pages of both sorts were byte-identical. Fix: computeTrendingScore now falls back to raw coverage counts (sourceCount/articleCount) when the claim-verified counts are zero/absent, so trending degrades gracefully to corroboration+recency instead of pure recency. Zero factual counts are treated as absent because a paused pipeline also writes zeros. Call site passes the resolved counts computed during preview sync. buildFeedQueryArgs extracted to apps/web/src/lib/feed-query.ts so the tab→query plumbing is unit-testable. README documents the finding and the resync/snapshot refresh paths. Tests: unit tests for the fallback matrix; convex-test regression proving sort=recent and sort=trending return different orderings on seeded coverage data and that sort defaults to trending; web unit test asserting the tab arg is passed through for all/topic feeds. Verified live on dev: resynced 80 previews via clustering:syncPublicEventPreviewForBackfill, rebuilt the feed snapshot, and the two tabs now return clearly different orderings. Codex review: no issues. Backend 132 pass, web 5 pass, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quiz is not launch-ready, so it is removed from the visible surface without deleting any quiz code. New apps/web/src/lib/feature-flags.ts holds a compile-time FEATURE_FLAGS.quiz = false; flipping it back on restores everything. - /quiz route: beforeLoad guard redirects to /feed (verified live: 307). - Header (desktop + mobile sheet) and MobileTabBar: quiz entry filtered out; tab bar grid drops to grid-cols-4 so no empty slot remains. - Feed + activity quiz CTA cards extracted into a shared QuizCta component that renders null while the flag is off. - Sitemap never listed /quiz; auth-redirect still allows it as a target, which now safely bounces through the route guard. Tests: flag default off; QuizCta renders nothing on both surfaces; header links and tab definitions contain no /quiz entry and keep a full grid; the route guard throws a redirect to /feed. Codex review: flagged the now-unused Link import in feed.tsx; fixed. Web suite 11 pass, backend 132 pass, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claim analysis is paused (BIV-602), so its UI must not ship. The event-detail tab region moved from routes/event.$slug.tsx into components/feed/event-detail-tabs.tsx, gated by a new FEATURE_FLAGS.claimAnalysis (default off, alongside the BIV-802 quiz flag): - Flag off (launch state): the perspectives content (perspective summaries, global impact, source coverage) renders directly with no leftover single-tab pill chrome — the layout looks intentional, not truncated. - Flag on: the original two-tab layout (Perspective / Analiza afirmațiilor) returns unchanged; EventClaimComparison was not deleted. Verified live on the dev server: event page renders 200 with zero "Analiza afirmațiilor" occurrences. Tests: component tests assert the claims tab/panel is absent and only the perspectives tablist renders; the remaining reformist/neutral/suveranist tabs render and switch correctly (Radix triggers activate on mousedown); single-summary events render with no tab bar at all. Codex review: no issues. Web 15 pass, backend 132 pass, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…other unnatural strings
Full audit of the shared RO catalog (packages/i18n) plus the backend's
user-visible Romanian fallbacks. Every change is documented old → new with
rationale in I18N_CHANGES.md, including a native-speaker review checklist and
a "reviewed and left as-is" list (bias, streak, coverage.left).
Highlights:
- "Cadrare reformistă/suveranistă" → "Formulare reformistă/suveranistă"
(framing is "formulare", not the "cadrare" calque); claim.framing(s)
likewise; docs/bias-axis-spec.md display-name table updated to match.
- Claim exclusives moved off left/right onto the reformist↔suveranist axis
vocabulary ("Exclusiv din stânga" → "Exclusiv reformist", etc.).
- Anglicisms/literals fixed: "Eșantion AI rolling" → "glisant",
"{count}% profunzime" → "derulat", "Mergi la început" → "Mergi la pagina
principală", "Fact check" → "Verificare factuală".
- Orthography normalized: e-mailul, feedul, streakurile (DOOM3 attached
articles), matching the catalog's dominant forms.
- Backend user-visible fallbacks de-calqued: LIMITED_COVERAGE_FALLBACK now
says "orientare"; the side-summary fallback ("perspectivă distinctă")
moved into prompts.ts as SIDE_COVERAGE_FALLBACK so tests can lint it.
LLM-internal prompt vocabulary (cadrareaSursei etc.) deliberately
unchanged — model-facing only, pinned by prompts.test.ts + the BIV-701
eval harness.
Tests: banned-term test over every RO catalog value (cadrare/încadrăr*/
exclusiv din stânga|dreapta|centru/rolling/feed-ul/streak-ur*) that fails if
any reappears; corrected labels pinned; RO/EN key parity + non-empty values;
backend test asserting the user-visible fallbacks avoid the calque.
Codex review: extended the banned-term regexes to încadrăr* inflections and
added backend fallback coverage, both as suggested. Backend 133 pass, web 19
pass, tsc clean everywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nted business details
All seven footer-linked pages (despre, contact, parteneri, cum-functioneaza,
sursele-noastre, politica-confidentialitate, termeni) replace the "Pagina
este în lucru" placeholders with accurate long-form Romanian prose describing
the actual product: bias-aware, multi-perspective Romanian news aggregation
on the reformist–suveranist axis, independent reliability scoring, automated
summaries with links to originals, honest limitations.
Constraints honored:
- No fabricated business identity: legal entity, address, contact e-mail and
effective dates are {{TODO: …}} placeholders, all collected in
FOOTER_TODO.md (with page + field) for later fill-in. Contact references
the real Resend/biviant.com e-mail setup without asserting an
unprovisioned support address.
- Privacy/terms describe the real stack and behavior: Convex, Vercel,
PostHog (EU), Resend, Google/Apple OAuth, 7-day unverified account
cleanup, GDPR rights incl. ANSPDCP, deletion cascade.
- Shared StaticPage/StaticSection layout, semantic tokens only.
- Dead placeholder i18n keys (page.*, placeholder.inProgress) removed from
both locales.
Tests: each footer page renders >500 chars of real content (no lorem, no
in-lucru placeholder); every {{TODO}} token in pages is mirrored in
FOOTER_TODO.md and vice versa; no CUI/phone/@biviant.com address appears
outside placeholders. All 7 routes verified live (200).
Codex review: 4 findings fixed — profile-deletion claim softened to match
the web flow ("o poți solicita"), PostHog deletion wording qualified +
launch ops check added to FOOTER_TODO.md, "metodologia completă"/"documentat
public" overclaims toned down, "datele lor" typo. Web 29 pass, backend 133
pass, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iability scores Balances the reformist-skewed source mix without degrading reliability integrity — axis score and reliabilityScore stay independently assigned, and presence in the feed list is never a credibility signal. Feeds (new tier 3, each fetch-verified live 2026-07-03): - Tier A: România TV (+3, rel 3 — canonical /feed endpoint; /rss 301s and the origin is slow), Realitatea Plus (+3, rel 3 — real feed lives at access/share/feeds/rss/homepage.xml; no /rss route). - Tier B: ActiveNews (+4, rel 2), Național (+3, rel 3). Feed ratio moves 8:2 → 8:6 reformist:suveranist (neutral 5) — slightly more balanced, deliberately not equal; reliability-weighted voice remains reformist-dominant (63 vs 22). Reputation-only rows (rated LOW, not ingested): Napoca News, Certitudinea, Națiunea (low-volume/opinion), Buciumul (failed live verification — feed intermittently empty). They resolve discovery-overlay articles to rated LOW sources instead of neutral defaults. Tier C disinformation nodes never ingested, bottom reliability 1: Flux24 and AzNews added, SolidNews lowered 2→1 (pro-Kremlin relay per the universul.net network investigation), OrtodoxINFO already 1. docs/source-balance-biv806.md records before/after ratios and per-source rationale. Dev deployment reseeded (7 created, 23 updated). Tests: pinned 8:5:6 feed mix (never equal/inverted); tier-3 feeds must be suveranist-leaning with reliability ≤4 and low credibility; Tier-C and reputation-only domains asserted out of ALL_FEEDS; per-tier reliability caps (A ≤4, B ≤3, C ≤1) on the reputation seed; live feed-parse smoke test per added URL (opt-in FEED_SMOKE=1; all 4 pass live). Codex review: 3 findings addressed (exact ratio pinned, reputation-only domains asserted out of feeds, Tier A cap tightened 5→4). Backend 140 pass, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ntic tokens
Surface 1 of the BIV-807 redesign, aligning web chrome with the native app's
DESIGN_LOG ("editorial calm" — typography and whitespace carry the design,
no glassmorphism, no scroll-linked chrome):
- Header: still masthead — hide-on-scroll removed (frequency law), flat
bg-background + bottom hairline instead of translucent backdrop-blur,
logo glow/hover-scale removed, h-16 → h-14. Nav active state is now
typographic (semibold foreground + heavier icon stroke) instead of
bg-primary/10 pills. Focus-visible outlines added; aria-current on
active links.
- MobileTabBar: flat full-width bar with top hairline replaces the
floating blurred pill (explicitly rejected in the native design log);
no hide-on-scroll, no tab-switch animation; active = foreground +
semibold + heavier stroke. Safe-area padding kept; same-tab tap still
scrolls to top.
- Layout offsets retuned: header spacer h-14, feed sticky bar md:top-14,
main pb-16, footer pb-20.
- Tokens: added --destructive-foreground (light+dark, matching native) and
--overlay/--overlay-foreground scrim tokens; the last four hardcoded
colors tokenized (sheet/drawer overlays, destructive button text,
event-card image chip). Destructive button gets dark:hover:bg-
destructive/70 so hover keeps AA contrast in dark mode (Codex finding).
Enforcement (ticket acceptance): new design-system.test.ts fails on ANY
hardcoded Tailwind color utility in web source (regex validated against
known-bad/known-good samples), asserts every light token has a .dark
counterpart (dark mode via tokens only), and pins the non-political
indigo/amber bias tokens.
Web 32 pass, tsc clean, live smoke on /feed and /despre (200).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Surface 2 of the BIV-807 redesign — the card pile becomes the editorial
list, matching the native app's feed anatomy (DESIGN_LOG):
- EventCard rewritten as an editorial row: kicker (topic, small caps) →
title (3-line clamp, semibold snug) → 4px bias distribution bar → meta
line ("n surse · m articole · acum X"), optional 80px right thumbnail.
Lead ("feature") row: full-width 3:2 image (only when an image exists),
25px headline, 2-line neutral summary. No card chrome, shadows, hover
lift, image zoom, source-logo stacks or per-row share/bookmark actions —
the feed is for reading. Search highlight and bias-bar aria preserved.
- Feed controls: the fixed, hide-on-scroll glassmorphic block (gradient +
backdrop-blur + shadow) is now a flat, in-flow header — the scroll-linked
machinery (ResizeObserver height sync, translate transforms,
useScrollVisibility) is deleted. Sort is a plain-text segmented control
(weight + color, not pills), aria-pressed kept.
- Feed lists: hairline divide-y rows; section headings demoted to
uppercase tracked zone labels; loading/empty states are typographic
lines instead of bordered cards; page gradient dropped.
- Saved page: same row anatomy as the feed (recognition over novelty) with
the bookmark toggle as the row's one action — rendered as a SIBLING of
the row link (no nested interactive elements); typographic header and
empty state.
Codex review: 3 findings fixed (unused cn import; bookmark button no
longer nested inside the Link; lead image frame renders only when an
image exists so no empty 3:2 box).
Tests: EventCard row-anatomy component tests (kicker/title/bar/meta, no
default actions, saved-page opt-in, lead summary, no chrome classes).
Web 37 pass, tsc clean, /feed + /salvate live smoke 200.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: zf.ro renders article bodies client-side and emits the real text in NewsArticle JSON-LD — but with raw control characters inside JSON strings, so JSON.parse failed. Extraction then fell back to generic block scoring, which picked the site-wide "Articole recomandate" teaser carousel (identical on every page) as the body of 38 unrelated articles. They embedded near-identically (cosine >= 0.85) and merged into one mega-event, with entities also derived from the shared boilerplate. Fixes: - parseJsonLdScripts: retry malformed JSON-LD with control chars replaced by spaces (lossless for valid JSON, recovers ZF's illegal output) - chooseBestContentBlock: prefer the publisher-declared JSON-LD articleBody over generic block scoring (per-article by definition), with a longer-wins escape when the DOM prose is >=1.5x fuller (some publishers put only the lede in articleBody); JSON-LD parsed once from the original html (stripNoise removes <script>, which had made the old post-scoring fallback dead code) - extractParagraphText: drop link-dominated paragraphs/blocks (linkTextRatio) so anchor-only teaser widgets never qualify as a body - demoteRepeatedSourceBodies: batch-level guard — the same body under different titles from one source is site furniture; demote those articles to title+snippet embeddings and re-derive summary/entities; grouped on a 2000-char body prefix so MAX_EMBEDDING_CHARS truncation under different-length titles can't evade the guard - migrations: dissolveMisclusteredEvent (requeue articles, clear stale summaries, tear down child rows via shared EVENT_CHILD_TABLES) and clearArticleSummariesByPrefix for leftover boilerplate summaries - singletonCleanup: export EVENT_CHILD_TABLES as the single source of truth for event child-table teardown Live repair already executed on dev: mega-event + residuals dissolved, 93 articles re-enriched and re-clustered, 5 stale summaries cleared; remaining multi-ZF events verified legitimate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: the router had no scrollRestoration configured, and TanStack
Router keeps the current scroll offset across SPA navigations by default —
so opening an event from a scrolled feed landed ~30% down. One route
(event detail) papered over it with a manual scrollTo effect; every other
route was exposed.
Fixes:
- router: enable scrollRestoration (fresh navigations start at top,
back/forward restores the saved position) with instant behavior
- router: work around two @tanstack/router-core@1.144 gaps, both fixed
upstream in 1.17x (delete the block on that upgrade):
1. a click within the 100ms scroll-writer throttle window attributes
the old page's offset to the NEW location, which then "restores"
mid-page — force PUSH navigations to the top (history traversals
and REPLACE are left to the router)
2. back/forward restore fires before data-driven routes have content,
so the scroll clamps near 0 and is never re-applied — re-apply the
stored offset as the page grows (100ms polls, 3s budget, cancelled
by any user input)
Constants come from an explicit @tanstack/router-core dep (pure values
only — Vite's dev prebundle duplicates the module, so its stateful
exports are dead copies here)
- event.$slug: drop the manual scrollTo bandaid (it fought back/forward
restoration)
- e2e: introduce Playwright (desktop + iPhone-13-viewport chromium
projects, reuses the local dev server) with regression specs: scrolled
feed -> event lands at top (including a deliberate fast-click inside
the throttle window), history back restores the feed position, direct
loads start at top
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…OS chrome The tab bar's CSS was already a correct fixed bottom bar (verified: no transformed ancestors, pinned at every scroll offset in emulation), but the viewport meta lacked viewport-fit=cover, so env(safe-area-inset-bottom) resolved to 0 on real iPhones and the bar's bottom row sat in the home-indicator area while Safari's toolbar collapsed during scroll. - add viewport-fit=cover; define a single html-level --safe-area-* token set (env() fallback lives in one place) consumed by the tab bar, drawer, bottom sheet, fixed header, main/footer clearances, and body padding - pad body top/left/right so flow content stays out of the notch/status bar - lift mobile toasts above the tab bar + home indicator (sonner mobileOffset) - e2e (mobile project): viewport meta regression, compiled-clearance guard (catches a dropped arbitrary-value calc), pinned-at-offsets across feed/static/event pages, no-containing-block ancestor guard, drawer open/close scroll preservation, footer-clears-bar check; shared gotoHydrated helper with the scroll-restoration spec Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web/src/components/feed/event.tsx (1)
1-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the unused duplicate
EventCardapps/web/src/components/feed/event.tsxisn’t imported anywhere; the feed routes useapps/web/src/components/feed/event-card.tsxinstead. Keeping both same-named components increases the chance of misimports and drift.🤖 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/feed/event.tsx` around lines 1 - 46, The `EventCard` component in `apps/web/src/components/feed/event.tsx` is a duplicate and unused, while the feed already uses `EventCard` from `event-card.tsx`. Remove this extra component or replace it with a re-export so there is only one source of truth, and update any references to continue pointing at the existing `EventCard` implementation.packages/backend/convex/ingestion.ts (1)
693-731: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared source-insert payload to remove duplication.
createSourceandgetOrCreateSourcenow share an identical args shape and an identicalctx.db.insert("sources", {...})payload (domain, name, bias vianamedAxisBias(baseBias), baseBias, reliabilityScore, provenance, logoUrl, mbfc*). This duplication will drift again the next time a field is added/changed.♻️ Proposed extraction
+function buildSourceDoc(args: { + domain: string; + name: string; + baseBias: number; + reliabilityScore: number; + provenance?: string; + mbfcCategory: string; + mbfcFactual?: string; + mbfcCredibility?: string; +}) { + return { + domain: args.domain, + name: args.name, + bias: namedAxisBias(args.baseBias), + baseBias: args.baseBias, + reliabilityScore: args.reliabilityScore, + provenance: args.provenance, + logoUrl: `https://icons.duckduckgo.com/ip3/${args.domain}.ico`, + mbfcCategory: args.mbfcCategory, + mbfcFactual: args.mbfcFactual, + mbfcCredibility: args.mbfcCredibility, + mbfcLastChecked: Date.now(), + }; +}Then both
createSourceandgetOrCreateSourcehandlers callctx.db.insert("sources", buildSourceDoc({ domain, name, baseBias, reliabilityScore, provenance, mbfcCategory, mbfcFactual, mbfcCredibility })).Also applies to: 734-778
🤖 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/ingestion.ts` around lines 693 - 731, Extract the duplicated sources insert logic shared by createSource and getOrCreateSource into a single helper that builds the full documents payload, including bias via namedAxisBias(baseBias), logoUrl, and mbfc fields. Update both handlers to call the new helper (for example, a buildSourceDoc function) before ctx.db.insert("sources", ...), so the args shape and insert payload stay in sync when fields change.
🤖 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/e2e/scroll-restoration.spec.ts`:
- Around line 65-81: The poll in scroll-restoration.spec.ts is coupled to
TanStack Router’s internal sessionStorage key and entry shape, so replace it
with a stable, user-facing synchronization check. Update the wait around the
scroll restoration flow to use observable page behavior or a test helper exposed
by the app instead of reading tsr-scroll-restoration-v1_3 and its window.scrollY
payload directly. Keep the change localized to the test logic in the
scroll-restoration spec so it no longer depends on router internals.
In `@apps/web/src/components/feed/event-detail-tabs.tsx`:
- Around line 138-140: The conditional return in event-detail-tabs.tsx
unnecessarily wraps perspectivesPanel in a plain div when
FEATURE_FLAGS.claimAnalysis is false. Update the rendering path in the
event-detail-tabs component to return the fragment content directly using a
React fragment or the existing perspectivesPanel JSX, rather than introducing an
extra DOM node, while keeping the same conditional behavior tied to
claimAnalysis.
In `@apps/web/src/components/header.tsx`:
- Around line 115-122: The SheetTitle in the header sheet is using the brand
name instead of describing the dialog’s purpose, so update the title content to
a meaningful navigation label in the header component. Keep the logo/brand
elsewhere if needed, but make the accessible name from SheetTitle describe the
menu purpose clearly, and adjust the surrounding SheetContent/SheetHeader
structure in header.tsx if necessary.
In `@apps/web/src/components/quiz-cta.tsx`:
- Around line 33-36: The BrainCircuit icon in the quiz CTA is decorative and
should be hidden from assistive tech; update the quiz-cta component to mark that
icon as aria-hidden="true" so screen readers only announce the nearby title
text, following the same pattern used in Header.tsx for nav icons.
In `@apps/web/src/components/SignInPrompt.tsx`:
- Around line 11-17: The SignInPrompt illustration prop is now a no-op, so
remove any dead call sites that still pass illustration JSX. Update the salvate
route caller in salvate.tsx to stop building and supplying the SVG block to
<SignInPrompt>, and sweep for any other SignInPrompt usages that still provide
illustration so they no longer carry unused markup.
In `@apps/web/src/routes/contact.tsx`:
- Around line 25-31: The contact page JSX still contains literal {{TODO}}
placeholders that will render to users, so replace them with finalized
contact/legal values before shipping or hide the page behind a feature flag
until they are ready. Update the content in the contact route component so the
email, legal entity, and registered address are no longer hardcoded TODO strings
and ensure the rendered `<p>` blocks only contain production-safe text.
In `@packages/backend/convex/lib/articleExtraction.ts`:
- Around line 1028-1047: `resolveVerifiedImageMetadata` currently verifies up to
three image candidates sequentially using `verifyImageUrl`, which can block a
worker for too long. Update the image verification path so this best-effort
hero-image check uses a shorter, dedicated timeout instead of the default long
timeout, and make it configurable through `ImageUrlVerifier` (or a wrapper used
by `resolveVerifiedImageMetadata`) so production can pass a tighter limit. Keep
the fix localized around `resolveVerifiedImageMetadata`, `ImageUrlVerifier`, and
`verifyImageUrl` so the main content-fetch flow is unaffected.
In `@packages/backend/convex/lib/imageVerification.ts`:
- Around line 87-120: The `verifyImageUrl` fetch path still allows internal
targets because it only validates the initial URL and then follows redirects
automatically. Update `verifyImageUrl` to block private/loopback/link-local
destinations by checking the final resolved URL (or each redirect hop) against a
denylist, or stop using `redirect: "follow"` and handle redirects manually. Keep
the guard near `fetchImpl`, `AbortController`, and the response handling so both
direct and redirected requests are rejected before `readLeadingBytes` runs.
In `@packages/backend/convex/migrations.ts`:
- Around line 378-383: The exported migration handlers clear sensitive image
fields without any visible authorization gate, so they can be invoked by
clients. Update clearHtmlPageImageUrls and clearHtmlPageImageForEventSlug in
migrations.ts to use internalMutation instead of mutation, or add the existing
admin authorization check before any destructive writes. Make sure the entry
points and related helpers keep the same behavior for authorized admin-only
execution while preventing public access.
In `@packages/backend/convex/topics.ts`:
- Around line 128-143: The prune logic is doing an unbounded full scan of the
users table inside the `pruneStale` mutation, which can exceed Convex
transaction limits as the dataset grows. Refactor the user cleanup in
`topics.ts` to process users in bounded batches or via a
scheduled/backfill-style job similar to `backfillEventTopicBatch`, using
pagination instead of `ctx.db.query("users").collect()`; keep the stale-topic
filtering and patching behavior the same, but ensure each transaction only
handles a limited slice of users.
In `@packages/i18n/src/strings.ts`:
- Around line 293-299: The English copy for onboarding.promise.cardSummary still
uses the old left/center/right framing, while the rest of the strings have moved
to the reformist/neutral/sovereigntist terminology. Update the
onboarding.promise.cardSummary entry in strings.ts so it matches the Romanian
wording and the broader terminology migration, using the same
onboarding.promise.* keys to locate it.
- Around line 384-398: Update the English claim-exclusivity copy in strings.ts
so the `claim.leftExclusive`, `claim.rightExclusive`, `claim.centerExclusive`,
and their `*Body` entries match the new reformist/sovereigntist terminology
already used elsewhere; keep the wording aligned with the Romanian locale and
the existing `event.*`, `coverage.*`, and `bias.*` vocabulary so
`claim.*Exclusive*` is consistent across the app.
---
Outside diff comments:
In `@apps/web/src/components/feed/event.tsx`:
- Around line 1-46: The `EventCard` component in
`apps/web/src/components/feed/event.tsx` is a duplicate and unused, while the
feed already uses `EventCard` from `event-card.tsx`. Remove this extra component
or replace it with a re-export so there is only one source of truth, and update
any references to continue pointing at the existing `EventCard` implementation.
In `@packages/backend/convex/ingestion.ts`:
- Around line 693-731: Extract the duplicated sources insert logic shared by
createSource and getOrCreateSource into a single helper that builds the full
documents payload, including bias via namedAxisBias(baseBias), logoUrl, and mbfc
fields. Update both handlers to call the new helper (for example, a
buildSourceDoc function) before ctx.db.insert("sources", ...), so the args shape
and insert payload stay in sync when fields change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 84904ca0-31a3-48cd-8b7e-451d6937b5f0
⛔ Files ignored due to path filters (3)
apps/native/ios/Podfile.lockis excluded by!**/*.lockpackages/backend/convex/_generated/api.d.tsis excluded by!**/_generated/**,!**/_generated/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!**/pnpm-lock.yaml
📒 Files selected for processing (90)
FOOTER_TODO.mdI18N_CHANGES.mdREADME.mdapps/web/.gitignoreapps/web/e2e/helpers.tsapps/web/e2e/horizontal-overflow.spec.tsapps/web/e2e/mobile-tab-bar.spec.tsapps/web/e2e/scroll-restoration.spec.tsapps/web/package.jsonapps/web/playwright.config.tsapps/web/src/components/SignInPrompt.tsxapps/web/src/components/auth-prompt-banner.tsxapps/web/src/components/auth-social.test.tsxapps/web/src/components/auth-social.tsxapps/web/src/components/feed/articles-list.tsxapps/web/src/components/feed/event-card.test.tsxapps/web/src/components/feed/event-card.tsxapps/web/src/components/feed/event-detail-tabs.test.tsxapps/web/src/components/feed/event-detail-tabs.tsxapps/web/src/components/feed/event.tsxapps/web/src/components/feed/source-coverage-summary.tsxapps/web/src/components/header.tsxapps/web/src/components/layout/Footer.tsxapps/web/src/components/layout/MobileTabBar.tsxapps/web/src/components/layout/StaticPage.tsxapps/web/src/components/quiz-cta.tsxapps/web/src/components/quiz-hidden.test.tsxapps/web/src/components/sign-in-form.tsxapps/web/src/components/sign-up-form.tsxapps/web/src/components/ui/button.tsxapps/web/src/components/ui/drawer.tsxapps/web/src/components/ui/sheet.tsxapps/web/src/design-system.test.tsapps/web/src/index.cssapps/web/src/lib/feature-flags.tsapps/web/src/lib/feed-query.test.tsapps/web/src/lib/feed-query.tsapps/web/src/lib/i18n/strings.test.tsapps/web/src/router.tsxapps/web/src/routes/__root.tsxapps/web/src/routes/activitate.tsxapps/web/src/routes/contact.tsxapps/web/src/routes/cum-functioneaza.tsxapps/web/src/routes/despre.tsxapps/web/src/routes/event.$slug.tsxapps/web/src/routes/feed.tsxapps/web/src/routes/footer-pages.test.tsxapps/web/src/routes/parteneri.tsxapps/web/src/routes/politica-confidentialitate.tsxapps/web/src/routes/quiz.tsxapps/web/src/routes/salvate.tsxapps/web/src/routes/source.$sourceId.tsxapps/web/src/routes/sursele-noastre.tsxapps/web/src/routes/termeni.tsxapps/web/vitest.config.tsdocs/bias-axis-spec.mddocs/source-balance-biv806.mdpackages/backend/convex/articleExtraction.test.tspackages/backend/convex/auth.test.tspackages/backend/convex/auth.tspackages/backend/convex/clustering.tspackages/backend/convex/enrichmentNode.tspackages/backend/convex/feeds.smoke.test.tspackages/backend/convex/feeds.test.tspackages/backend/convex/feeds.tspackages/backend/convex/imageSniff.test.tspackages/backend/convex/imageVerification.test.tspackages/backend/convex/ingestion.tspackages/backend/convex/lib/articleExtraction.tspackages/backend/convex/lib/imageSniff.tspackages/backend/convex/lib/imageVerification.tspackages/backend/convex/lib/publicEventPreviews.tspackages/backend/convex/lib/romanian.tspackages/backend/convex/lib/userProfile.tspackages/backend/convex/migrations.tspackages/backend/convex/prompts.test.tspackages/backend/convex/prompts.tspackages/backend/convex/publicEventPreviews.test.tspackages/backend/convex/romanian.test.tspackages/backend/convex/seeds.tspackages/backend/convex/shareAssetsNode.tspackages/backend/convex/singletonCleanup.tspackages/backend/convex/sourceReputation.test.tspackages/backend/convex/sourceReputation.tspackages/backend/convex/summarizationNode.tspackages/backend/convex/topicCatalog.test.tspackages/backend/convex/topicCatalog.tspackages/backend/convex/topics.tspackages/backend/package.jsonpackages/i18n/src/strings.ts
| await expect | ||
| .poll( | ||
| () => | ||
| page.evaluate(() => { | ||
| const byKey = JSON.parse( | ||
| window.sessionStorage.getItem("tsr-scroll-restoration-v1_3") || | ||
| "{}", | ||
| ); | ||
| return Object.values(byKey).some( | ||
| (entry) => | ||
| ((entry as { window?: { scrollY: number } }).window?.scrollY ?? | ||
| 0) > 300, | ||
| ); | ||
| }), | ||
| { timeout: 5_000 }, | ||
| ) | ||
| .toBe(true); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff
Test couples to TanStack Router's internal sessionStorage key/shape.
Polling window.sessionStorage.getItem("tsr-scroll-restoration-v1_3") and its {window: {scrollY}} shape reaches into an undocumented internal storage format. A router version bump that changes the storage key or entry shape would silently break this synchronization wait (the poll would never resolve, timing out the test) without any corresponding app behavior actually being broken.
🤖 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/e2e/scroll-restoration.spec.ts` around lines 65 - 81, The poll in
scroll-restoration.spec.ts is coupled to TanStack Router’s internal
sessionStorage key and entry shape, so replace it with a stable, user-facing
synchronization check. Update the wait around the scroll restoration flow to use
observable page behavior or a test helper exposed by the app instead of reading
tsr-scroll-restoration-v1_3 and its window.scrollY payload directly. Keep the
change localized to the test logic in the scroll-restoration spec so it no
longer depends on router internals.
| if (staleTopicIds.size > 0) { | ||
| const users = await ctx.db.query("users").collect(); | ||
| for (const user of users) { | ||
| const followedTopicIds = user.followedTopicIds ?? []; | ||
| const nextFollowedTopicIds = followedTopicIds.filter( | ||
| (topicId: Id<"topics">) => !staleTopicIds.has(String(topicId)), | ||
| ); | ||
| if (nextFollowedTopicIds.length !== followedTopicIds.length) { | ||
| await ctx.db.patch(user._id, { | ||
| followedTopicIds: nextFollowedTopicIds, | ||
| }); | ||
| removedFollowedTopicRefs += | ||
| followedTopicIds.length - nextFollowedTopicIds.length; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial
Unbounded users table scan inside one mutation transaction.
The prune path runs ctx.db.query("users").collect() (and per-stale-topic eventTopics scans) within a single Convex mutation. Convex enforces per-transaction read/scan limits, so on a grown users table this whole pruneStale run can hit the limit and fail atomically. Consider paginating the user cleanup (or moving prune into a batched/scheduled job like backfillEventTopicBatch) rather than a full collect() in one transaction.
As per path instructions: "Focus on Convex best practices".
🤖 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/topics.ts` around lines 128 - 143, The prune logic is
doing an unbounded full scan of the users table inside the `pruneStale`
mutation, which can exceed Convex transaction limits as the dataset grows.
Refactor the user cleanup in `topics.ts` to process users in bounded batches or
via a scheduled/backfill-style job similar to `backfillEventTopicBatch`, using
pagination instead of `ctx.db.query("users").collect()`; keep the stale-topic
filtering and patching behavior the same, but ensure each transaction only
handles a limited slice of users.
Source: Path instructions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
apps/native/app/source/[sourceId].tsx (1)
268-284: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGeneric label/text loses per-row distinguishability in a list.
Swapping
article.event.titlefor a fixedt("source.relatedEvent")string means every article row's "related event" link now has identical accessible name and visible text, even though each points to a different event. Screen reader users navigating the list (and sighted users skimming) can no longer tell which event a given article is clustered with without activating the link.Consider keeping the event title visible/announced alongside the localized label, e.g.
t("source.relatedEvent")as a prefix plusarticle.event.title.🩹 Suggested fix retaining localization and per-row distinctness
<Pressable accessibilityRole="button" - accessibilityLabel={t("source.relatedEvent")} + accessibilityLabel={`${t("source.relatedEvent")}: ${article.event.title}`} onPress={() => router.push(`/event/${article.event!.slug}`)} hitSlop={6} className="min-h-9 max-w-full flex-row items-center gap-1 active:opacity-70" > <Icon name="albums-outline" size={13} className="text-primary" /> <Text numberOfLines={1} className="shrink text-sm font-medium text-primary" > - {t("source.relatedEvent")} + {article.event.title} </Text> </Pressable>🤖 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/native/app/source/`[sourceId].tsx around lines 268 - 284, The related event link in the `[sourceId]` article row is now using a generic label, so each row becomes indistinguishable to users and assistive tech. Update the `article.event` Pressable in the source screen to keep the localized `t("source.relatedEvent")` text while also including `article.event.title` in both the visible text and accessible label, so each item remains unique. Make the change in the `Pressable`/`Text` block that renders the related event link, preserving localization and row-specific context.Source: Path instructions
apps/web/src/components/feed/event-card.tsx (1)
43-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDrop the unused
maxSourcesprop fromEventCardProps. Nothing in this repo passes it, and the component never reads it.🤖 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/feed/event-card.tsx` around lines 43 - 50, Drop the unused maxSources prop from EventCardProps in event-card.tsx and remove its compatibility comment, since EventCard never reads it and nothing in the repo passes it. Update the EventCardProps interface and any related prop handling or destructuring in EventCard so the component only exposes the remaining used props like variant, searchQuery, returnToFeed, interactive, and showBookmark.apps/web/src/routes/source.$sourceId.tsx (1)
336-357: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRestore a fallback for unclustered articles. When
article.eventis missing, this section now disappears entirely, so users lose the previous clustering state signal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/routes/source`.$sourceId.tsx around lines 336 - 357, The article action cluster is now only rendered when article.event exists, so unclustered articles lose the previous state signal; update the Source route rendering in the related article card to keep a fallback element for the missing-event case instead of hiding the whole section, using the existing article/event link area and the article.event check as the place to add the fallback state.apps/web/src/routes/activitate.tsx (1)
362-406: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winDuplicate card markup — reuse
RecentReadingIteminstead of re-implementing it inline.This block reimplements almost the exact same thumbnail/title/timestamp/source-count layout that was just extracted into
RecentReadingItem(apps/web/src/components/activity/recent-reading-item.tsx) for the reading-history list above. The only differences are the timestamp field name (bookmarkedAtvslastViewedAt) and the fallback icon (BookmarkvsNewspaper). Generalizing the extracted component (e.g. accepttimestamp/fallbackIconprops) would remove this duplication and keep both lists in sync going forward.♻️ Suggested direction
-export function RecentReadingItem({ entry }: { entry: RecentReadingEntry }) { +export function RecentReadingItem({ + entry, + timestamp, + fallbackIcon: FallbackIcon = Newspaper, +}: { + entry: RecentReadingEntry; + timestamp: number; + fallbackIcon?: LucideIcon; +}) { ... - <span>{formatRelativeTimestamp(entry.lastViewedAt, locale)}</span> + <span>{formatRelativeTimestamp(timestamp, locale)}</span>Then in
activitate.tsx:- {recentBookmarks.slice(0, 4).map((entry) => ( - <Link ... > ... </Link> - ))} + {recentBookmarks.slice(0, 4).map((entry) => ( + <RecentReadingItem + key={entry.event._id} + entry={entry} + timestamp={entry.bookmarkedAt} + fallbackIcon={Bookmark} + /> + ))}🤖 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/activitate.tsx` around lines 362 - 406, The bookmarked events card markup is duplicated instead of reusing RecentReadingItem, so update the extracted component to cover both cases. Extend RecentReadingItem to accept a generic timestamp prop and a configurable fallback icon/label, then use it in the recentBookmarks mapping in activitate.tsx rather than re-implementing the thumbnail/title/timestamp/source-count layout inline. Keep the existing source-count and title rendering behavior, and align the bookmarkedAt vs lastViewedAt difference through the shared component API.packages/backend/convex/auth.ts (1)
524-533: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRetry profile-heal failures instead of only logging them
A transient failure here still leaves legacy users withoutusers/userStatsuntil a later sign-in or backfill runs, so profile-gated pages can still see them as signed out.🤖 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/auth.ts` around lines 524 - 533, The profile-heal flow in onCreate currently only logs failures, so transient errors can leave legacy users without their users/userStats records. Update healUserProfileForSession to retry the lookup-and-heal path on failure instead of just swallowing/logging it, and make the onCreate handler in auth.ts await that retry behavior so a temporary backend issue doesn’t leave the session half-initialized. Keep the fix localized around healUserProfileForSession and the onCreate callback that calls it.
🤖 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/lib/theme/ThemeProvider.tsx`:
- Around line 60-80: The ThemeProvider useEffect is re-subscribing the
matchMedia change listener whenever preference changes, because the effect
depends on preference. Update ThemeProvider so the listener is registered once
on mount and the handler reads the latest preference from a ref (or equivalent)
inside handleChange, while keeping resolveThemePreference, setResolvedTheme, and
applyResolvedTheme behavior unchanged.
In `@docs/code-audit-2026-07.md`:
- Around line 16-24: The audit note is disclosing exploit-level details about
the still-open `migrations.ts` vulnerability, so tone it down or move the
specifics out of committed docs until the fix lands. Update the
`docs/code-audit-2026-07.md` summary to describe the risk at a high level only,
and keep the exact destructive-operation list and attack path in a restricted
tracker or behind the same PR that changes `migrations.ts` to `internalMutation`
or gates it with `requireAdminUser`. Also adjust the related section referenced
by the “Also applies to” note so no actionable exploit roadmap remains in the
repository.
---
Outside diff comments:
In `@apps/native/app/source/`[sourceId].tsx:
- Around line 268-284: The related event link in the `[sourceId]` article row is
now using a generic label, so each row becomes indistinguishable to users and
assistive tech. Update the `article.event` Pressable in the source screen to
keep the localized `t("source.relatedEvent")` text while also including
`article.event.title` in both the visible text and accessible label, so each
item remains unique. Make the change in the `Pressable`/`Text` block that
renders the related event link, preserving localization and row-specific
context.
In `@apps/web/src/components/feed/event-card.tsx`:
- Around line 43-50: Drop the unused maxSources prop from EventCardProps in
event-card.tsx and remove its compatibility comment, since EventCard never reads
it and nothing in the repo passes it. Update the EventCardProps interface and
any related prop handling or destructuring in EventCard so the component only
exposes the remaining used props like variant, searchQuery, returnToFeed,
interactive, and showBookmark.
In `@apps/web/src/routes/activitate.tsx`:
- Around line 362-406: The bookmarked events card markup is duplicated instead
of reusing RecentReadingItem, so update the extracted component to cover both
cases. Extend RecentReadingItem to accept a generic timestamp prop and a
configurable fallback icon/label, then use it in the recentBookmarks mapping in
activitate.tsx rather than re-implementing the
thumbnail/title/timestamp/source-count layout inline. Keep the existing
source-count and title rendering behavior, and align the bookmarkedAt vs
lastViewedAt difference through the shared component API.
In `@apps/web/src/routes/source`.$sourceId.tsx:
- Around line 336-357: The article action cluster is now only rendered when
article.event exists, so unclustered articles lose the previous state signal;
update the Source route rendering in the related article card to keep a fallback
element for the missing-event case instead of hiding the whole section, using
the existing article/event link area and the article.event check as the place to
add the fallback state.
In `@packages/backend/convex/auth.ts`:
- Around line 524-533: The profile-heal flow in onCreate currently only logs
failures, so transient errors can leave legacy users without their
users/userStats records. Update healUserProfileForSession to retry the
lookup-and-heal path on failure instead of just swallowing/logging it, and make
the onCreate handler in auth.ts await that retry behavior so a temporary backend
issue doesn’t leave the session half-initialized. Keep the fix localized around
healUserProfileForSession and the onCreate callback that calls it.
🪄 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: a8916bb6-93b5-4ec2-9143-4dd7e3c93776
⛔ Files ignored due to path filters (1)
packages/backend/convex/_generated/api.d.tsis excluded by!**/_generated/**,!**/_generated/**
📒 Files selected for processing (55)
apps/native/app/(tabs)/index.tsxapps/native/app/source/[sourceId].tsxapps/web/e2e/theme.spec.tsapps/web/src/components/activity/recent-reading-item.test.tsxapps/web/src/components/activity/recent-reading-item.tsxapps/web/src/components/current-month-reading-calendar.test.tsxapps/web/src/components/current-month-reading-calendar.tsxapps/web/src/components/feed/articles-list.tsxapps/web/src/components/feed/event-card.test.tsxapps/web/src/components/feed/event-card.tsxapps/web/src/components/feed/event-claim-comparison.tsxapps/web/src/components/feed/event-detail-tabs.tsxapps/web/src/components/feed/source-coverage-summary.tsxapps/web/src/components/header.tsxapps/web/src/components/layout/Footer.tsxapps/web/src/components/profile/AnonymousProfile.tsxapps/web/src/components/profile/AuthenticatedProfile.tsxapps/web/src/components/streak-activity-calendar.tsxapps/web/src/components/theme/ThemePicker.tsxapps/web/src/components/ui/section-title.test.tsxapps/web/src/components/ui/section-title.tsxapps/web/src/design-system.test.tsapps/web/src/index.cssapps/web/src/lib/i18n/strings.tsapps/web/src/lib/seo.tsapps/web/src/lib/theme/ThemeProvider.test.tsxapps/web/src/lib/theme/ThemeProvider.tsxapps/web/src/lib/theme/index.test.tsapps/web/src/lib/theme/index.tsapps/web/src/routes/__root.tsxapps/web/src/routes/activitate.tsxapps/web/src/routes/contact.tsxapps/web/src/routes/cum-functioneaza.tsxapps/web/src/routes/despre.tsxapps/web/src/routes/feed.tsxapps/web/src/routes/footer-pages.test.tsxapps/web/src/routes/parteneri.tsxapps/web/src/routes/politica-confidentialitate.tsxapps/web/src/routes/quiz.tsxapps/web/src/routes/salvate.tsxapps/web/src/routes/source.$sourceId.tsxapps/web/src/routes/sursele-noastre.tsxapps/web/src/routes/termeni.tsxapps/web/src/routes/unsubscribe.tsxdocs/code-audit-2026-07.mdpackages/backend/convex/auth.tspackages/backend/convex/brand.tspackages/backend/convex/config.tspackages/backend/convex/emails.tspackages/backend/convex/interactions.tspackages/backend/convex/prompts.tspackages/backend/convex/quizNode.tspackages/i18n/src/brand.tspackages/i18n/src/index.tspackages/i18n/src/strings.ts
💤 Files with no reviewable changes (1)
- apps/web/src/components/streak-activity-calendar.tsx
| useEffect(() => { | ||
| const media = window.matchMedia?.(THEME_MEDIA_QUERY); | ||
| if (!media) { | ||
| return; | ||
| } | ||
|
|
||
| const handleChange = () => { | ||
| const nextResolvedTheme = resolveThemePreference( | ||
| preference, | ||
| getSystemPrefersDark(), | ||
| ); | ||
|
|
||
| setResolvedTheme(nextResolvedTheme); | ||
| applyResolvedTheme(nextResolvedTheme); | ||
| }; | ||
|
|
||
| media.addEventListener("change", handleChange); | ||
| return () => { | ||
| media.removeEventListener("change", handleChange); | ||
| }; | ||
| }, [preference]); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
Minor: effect resubscribes matchMedia listener on every preference change.
Depending on [preference] re-runs the effect (remove + re-add listener) each time the user changes preference. Using a ref for preference inside handleChange would let the effect run once on mount.
♻️ Optional refactor using a ref
+ const preferenceRef = useRef(preference);
+ preferenceRef.current = preference;
+
useEffect(() => {
const media = window.matchMedia?.(THEME_MEDIA_QUERY);
if (!media) {
return;
}
const handleChange = () => {
const nextResolvedTheme = resolveThemePreference(
- preference,
+ preferenceRef.current,
getSystemPrefersDark(),
);
setResolvedTheme(nextResolvedTheme);
applyResolvedTheme(nextResolvedTheme);
};
media.addEventListener("change", handleChange);
return () => {
media.removeEventListener("change", handleChange);
};
- }, [preference]);
+ }, []);📝 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.
| useEffect(() => { | |
| const media = window.matchMedia?.(THEME_MEDIA_QUERY); | |
| if (!media) { | |
| return; | |
| } | |
| const handleChange = () => { | |
| const nextResolvedTheme = resolveThemePreference( | |
| preference, | |
| getSystemPrefersDark(), | |
| ); | |
| setResolvedTheme(nextResolvedTheme); | |
| applyResolvedTheme(nextResolvedTheme); | |
| }; | |
| media.addEventListener("change", handleChange); | |
| return () => { | |
| media.removeEventListener("change", handleChange); | |
| }; | |
| }, [preference]); | |
| const preferenceRef = useRef(preference); | |
| preferenceRef.current = preference; | |
| useEffect(() => { | |
| const media = window.matchMedia?.(THEME_MEDIA_QUERY); | |
| if (!media) { | |
| return; | |
| } | |
| const handleChange = () => { | |
| const nextResolvedTheme = resolveThemePreference( | |
| preferenceRef.current, | |
| getSystemPrefersDark(), | |
| ); | |
| setResolvedTheme(nextResolvedTheme); | |
| applyResolvedTheme(nextResolvedTheme); | |
| }; | |
| media.addEventListener("change", handleChange); | |
| return () => { | |
| media.removeEventListener("change", handleChange); | |
| }; | |
| }, []); |
🤖 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/theme/ThemeProvider.tsx` around lines 60 - 80, The
ThemeProvider useEffect is re-subscribing the matchMedia change listener
whenever preference changes, because the effect depends on preference. Update
ThemeProvider so the listener is registered once on mount and the handler reads
the latest preference from a ref (or equivalent) inside handleChange, while
keeping resolveThemePreference, setResolvedTheme, and applyResolvedTheme
behavior unchanged.
| **The 5 most dangerous problems:** | ||
|
|
||
| 1. **`migrations.ts` exposes 13 destructive operations as *public, unauthenticated* mutations** (Critical). Any client holding the Convex URL — which ships embedded in the web and native bundles — can wipe waitlist rows, rewrite every article/event, delete embeddings, and trigger full-table AI re-scoring that cascades automatically. This is the single most serious finding. | ||
| 2. **No CI exists** (High). No `.github/workflows/`; nothing gates tests, type-checks, or dependency audits on a change. | ||
| 3. **`turbo check-types` silently covers only 2 of 5 packages** (High). Web and backend have no `check-types` script, so the web app currently sits on `main` with **6 real type errors** that a green `pnpm check-types` hides. | ||
| 4. **83 dependency vulnerabilities, 2 critical** (High) — `protobufjs` (RCE) and `shell-quote` — with no audit gate to catch them. | ||
| 5. **Unbounded `.collect()` of the append-only interaction log** on the dashboard and bookmarks paths (Medium), which have no retention cron — a latent scale cliff. | ||
|
|
||
| **Single highest-leverage fix:** convert every `migrations.ts` function to `internalMutation` (or gate with `requireAdminUser`). It closes the Critical hole in one edit. The second-highest-leverage fix is adding a CI workflow that runs `tsc` across all packages, the vitest suites, and `pnpm audit` — that surfaces findings #2, #3, and the web type errors and prevents recurrence. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Avoid committing exploit-level vulnerability detail while the hole is still open.
This audit pinpoints, with file/line/impact detail, a still-open Critical unauthenticated-mutation vulnerability exposes 13 destructive operations as public, unauthenticated mutations (Critical). Any client holding the Convex URL — which ships embedded in the web and native bundles — can wipe waitlist rows, rewrite every article/event, delete embeddings, and trigger full-table AI re-scoring that cascades automatically. Shipping this level of exploit detail into source control before the fix lands hands anyone with repo access (current or future collaborators, CI logs, accidental public exposure) a ready-made attack roadmap.
Recommend fixing C1 first (or landing the fix in the same PR), and/or keeping granular exploit specifics in a restricted-access tracker rather than a committed markdown file until remediated.
Also applies to: 66-87
🧰 Tools
🪛 LanguageTool
[uncategorized] ~19-~19: The official name of this software platform is spelled with a capital “H”.
Context: ...finding. 2. No CI exists (High). No .github/workflows/; nothing gates tests, type-...
(GITHUB)
🤖 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 `@docs/code-audit-2026-07.md` around lines 16 - 24, The audit note is
disclosing exploit-level details about the still-open `migrations.ts`
vulnerability, so tone it down or move the specifics out of committed docs until
the fix lands. Update the `docs/code-audit-2026-07.md` summary to describe the
risk at a high level only, and keep the exact destructive-operation list and
attack path in a restricted tracker or behind the same PR that changes
`migrations.ts` to `internalMutation` or gates it with `requireAdminUser`. Also
adjust the related section referenced by the “Also applies to” note so no
actionable exploit roadmap remains in the repository.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
apps/web/src/components/feed/event-card.tsx (1)
217-220: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFeature thumbnail duplicates the headline for screen readers.
The feature image uses
alt={event.title}while the list variant (Line 247) correctly marks its thumbnail decorative (alt="" aria-hidden). Since the<h3>already announces the title within the same link, the feature image's descriptive alt causes the headline to be read twice. Make it decorative for consistency.♿ Proposed fix
<img src={event.imageUrl} - alt={event.title} + alt="" + aria-hidden="true" className="h-full w-full object-cover" loading="lazy" />🤖 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/feed/event-card.tsx` around lines 217 - 220, The feature thumbnail in the event card is redundantly announcing the event title to screen readers because the surrounding link already contains the heading, so update the image in the feature variant of event-card to be decorative like the list variant by using the same empty-alt/hidden treatment. Locate the image markup in the event-card component near the feature layout and align its accessibility props with the existing thumbnail pattern used elsewhere in the component.Source: Path instructions
apps/web/src/components/layout/Footer.tsx (1)
37-37: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse spaces in the
calc()value.pb-[calc(5rem+var(--safe-area-bottom))]is emitted as an invalidcalc(), so the mobile bottom padding is dropped. Change it topb-[calc(5rem_+_var(--safe-area-bottom))].Suggested fix
- <footer className="border-t border-border bg-muted/20 pb-[calc(5rem+var(--safe-area-bottom))] md:pb-8"> + <footer className="border-t border-border bg-muted/20 pb-[calc(5rem_+_var(--safe-area-bottom))] md:pb-8">🤖 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/layout/Footer.tsx` at line 37, The Footer component uses an invalid Tailwind arbitrary value in the footer padding class, causing the mobile bottom padding to be dropped. Update the `footer` element in `Footer.tsx` to use a valid spaced `calc()` expression in the className, specifically the `pb-[calc(...)]` value on the footer, so the safe-area bottom padding is applied correctly.packages/backend/convex/ingestion.ts (1)
1849-1878: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winNormalize overlay titles/snippets before inserting. The overlay path stores raw
item.title/item.snippet, while the main feed path normalizes both first. That leaves overlay rows inconsistent with the curated path and can skew same-source dedup when headline suffixes are stripped in one path but not the other.🤖 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/ingestion.ts` around lines 1849 - 1878, The overlay insert path in ingestion should normalize article text the same way the main feed path does before calling internal.ingestion.insertArticles. Update the block that builds the article object from item.title and item.snippet to pass them through the same title/snippet normalization used elsewhere in ingestion so overlay rows stay consistent and dedup behavior matches across paths.apps/web/src/routes/politica-confidentialitate.tsx (1)
24-35: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDangling reference to removed "operator" contact section.
The "Drepturile tale" section tells users to use "datele de contact de mai sus" for exercising GDPR rights, but the only section that contained contact/operator details (lines 24-35) is commented out. As shipped, there is no contact information above this text anywhere on the page.
🐛 Proposed fix
- <p> - Conform Regulamentului (UE) 2016/679 (GDPR), ai dreptul de acces, - rectificare, ștergere, restricționare, portabilitate și opoziție, - precum și dreptul de a depune o plângere la Autoritatea Națională de - Supraveghere a Prelucrării Datelor cu Caracter Personal (ANSPDCP). - Pentru exercitarea drepturilor, folosește datele de contact de mai - sus. - </p> + <p> + Conform Regulamentului (UE) 2016/679 (GDPR), ai dreptul de acces, + rectificare, ștergere, restricționare, portabilitate și opoziție, + precum și dreptul de a depune o plângere la Autoritatea Națională de + Supraveghere a Prelucrării Datelor cu Caracter Personal (ANSPDCP). + Pentru exercitarea drepturilor, scrie-ne prin pagina de{" "} + <Link to="/contact" className="underline hover:text-foreground"> + Contact + </Link> + . + </p>Also applies to: 95-104
🤖 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/politica-confidentialitate.tsx` around lines 24 - 35, Restore the missing contact context for the “Drepturile tale” section in politika-confidentialitate.tsx by either uncommenting and filling the operator/contact block around the StaticSection or updating the rights text to point to an existing contact source on the page. Make sure the phrasing “datele de contact de mai sus” is only kept if the referenced contact details actually appear earlier in the rendered content, and check the related “Drepturile tale” copy so it no longer depends on the removed operator section.apps/native/app/source/[sourceId].tsx (1)
268-284: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winGeneric label makes related-event rows indistinguishable in a list.
Every article row's "related event" control now shares the same accessibilityLabel/text (
t("source.relatedEvent")), whereas it previously usedarticle.event.title. In a FlashList with many articles, screen-reader users can no longer tell which event each control links to when navigating by accessible name.♿ Suggested fix to restore per-item context
<Pressable accessibilityRole="button" - accessibilityLabel={t("source.relatedEvent")} + accessibilityLabel={`${t("source.relatedEvent")}: ${article.event!.title}`} onPress={() => router.push(`/event/${article.event!.slug}`)} hitSlop={6} className="min-h-9 max-w-full flex-row items-center gap-1 active:opacity-70" > <Icon name="albums-outline" size={13} className="text-primary" /> <Text numberOfLines={1} className="shrink text-sm font-medium text-primary" > {t("source.relatedEvent")} </Text> </Pressable>🤖 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/native/app/source/`[sourceId].tsx around lines 268 - 284, The related-event control in the article row is using a generic accessible name, so each FlashList item becomes indistinguishable to screen readers. Update the Pressable in the [sourceId] article row to use per-item event context from article.event (for both the accessibilityLabel and visible text as appropriate), rather than only t("source.relatedEvent"), so each row can be uniquely identified. Keep the existing navigation behavior in the onPress handler, but make the accessible name include the event title from the article.event object.Source: Path instructions
🤖 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/routes/__root.tsx`:
- Line 113: The hardcoded theme-color meta entry in __root.tsx only matches dark
mode and won’t adapt for the new light theme. Update the meta tags in the root
route to provide separate light and dark theme-color values using
media="(prefers-color-scheme: ...)" so browser chrome matches the active scheme.
If the app’s ThemeProvider supports explicit overrides beyond OS preference,
also wire theme-color updates to that theme state so the meta color stays in
sync with user selection.
In `@packages/backend/convex/lib/imageVerification.ts`:
- Around line 66-71: The IPv6 unique-local guard in imageVerification logic is
over-broad because the host checks in the function that handles reachability
treat any hostname starting with “fc” or “fd” as blocked. Update the host
validation so the `fc`/`fd` prefixes are only applied when the value is clearly
an IPv6 literal, such as by requiring a colon in the host before matching those
prefixes. Keep the existing `::1`, `::`, and `fe80:` checks in the same helper,
but narrow the unique-local detection so ordinary DNS names like `fc2.com` or
`fda.gov` are no longer rejected.
---
Outside diff comments:
In `@apps/native/app/source/`[sourceId].tsx:
- Around line 268-284: The related-event control in the article row is using a
generic accessible name, so each FlashList item becomes indistinguishable to
screen readers. Update the Pressable in the [sourceId] article row to use
per-item event context from article.event (for both the accessibilityLabel and
visible text as appropriate), rather than only t("source.relatedEvent"), so each
row can be uniquely identified. Keep the existing navigation behavior in the
onPress handler, but make the accessible name include the event title from the
article.event object.
In `@apps/web/src/components/feed/event-card.tsx`:
- Around line 217-220: The feature thumbnail in the event card is redundantly
announcing the event title to screen readers because the surrounding link
already contains the heading, so update the image in the feature variant of
event-card to be decorative like the list variant by using the same
empty-alt/hidden treatment. Locate the image markup in the event-card component
near the feature layout and align its accessibility props with the existing
thumbnail pattern used elsewhere in the component.
In `@apps/web/src/components/layout/Footer.tsx`:
- Line 37: The Footer component uses an invalid Tailwind arbitrary value in the
footer padding class, causing the mobile bottom padding to be dropped. Update
the `footer` element in `Footer.tsx` to use a valid spaced `calc()` expression
in the className, specifically the `pb-[calc(...)]` value on the footer, so the
safe-area bottom padding is applied correctly.
In `@apps/web/src/routes/politica-confidentialitate.tsx`:
- Around line 24-35: Restore the missing contact context for the “Drepturile
tale” section in politika-confidentialitate.tsx by either uncommenting and
filling the operator/contact block around the StaticSection or updating the
rights text to point to an existing contact source on the page. Make sure the
phrasing “datele de contact de mai sus” is only kept if the referenced contact
details actually appear earlier in the rendered content, and check the related
“Drepturile tale” copy so it no longer depends on the removed operator section.
In `@packages/backend/convex/ingestion.ts`:
- Around line 1849-1878: The overlay insert path in ingestion should normalize
article text the same way the main feed path does before calling
internal.ingestion.insertArticles. Update the block that builds the article
object from item.title and item.snippet to pass them through the same
title/snippet normalization used elsewhere in ingestion so overlay rows stay
consistent and dedup behavior matches across paths.
🪄 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: 5d10073f-3916-4e3b-b7be-7e78b089a4ba
⛔ Files ignored due to path filters (6)
apps/web/public/logo-biviant-dark.pngis excluded by!**/*.pngapps/web/public/logo-biviant.pngis excluded by!**/*.pngapps/web/public/logo-mark.pngis excluded by!**/*.pngapps/web/public/og-image.jpgis excluded by!**/*.jpgapps/web/public/og-image.pngis excluded by!**/*.pngpackages/backend/convex/_generated/api.d.tsis excluded by!**/_generated/**,!**/_generated/**
📒 Files selected for processing (42)
apps/native/app/(tabs)/index.tsxapps/native/app/source/[sourceId].tsxapps/web/src/components/BrandLogo.tsxapps/web/src/components/SignInPrompt.tsxapps/web/src/components/feed/event-card.tsxapps/web/src/components/feed/event-claim-comparison.tsxapps/web/src/components/feed/event-detail-tabs.tsxapps/web/src/components/feed/event.tsxapps/web/src/components/header.tsxapps/web/src/components/layout/Footer.tsxapps/web/src/components/quiz-cta.tsxapps/web/src/components/theme/ThemeToggle.tsxapps/web/src/lib/i18n/strings.tsapps/web/src/lib/seo.tsapps/web/src/routes/__root.tsxapps/web/src/routes/contact.tsxapps/web/src/routes/cum-functioneaza.tsxapps/web/src/routes/despre.tsxapps/web/src/routes/footer-pages.test.tsxapps/web/src/routes/parteneri.tsxapps/web/src/routes/politica-confidentialitate.tsxapps/web/src/routes/quiz.tsxapps/web/src/routes/salvate.tsxapps/web/src/routes/source.$sourceId.tsxapps/web/src/routes/sursele-noastre.tsxapps/web/src/routes/termeni.tsxapps/web/src/routes/unsubscribe.tsxpackages/backend/convex/auth.tspackages/backend/convex/brand.tspackages/backend/convex/config.tspackages/backend/convex/emails.tspackages/backend/convex/enrichmentNode.tspackages/backend/convex/imageVerification.test.tspackages/backend/convex/ingestion.tspackages/backend/convex/lib/articleExtraction.tspackages/backend/convex/lib/imageVerification.tspackages/backend/convex/migrations.tspackages/backend/convex/prompts.tspackages/backend/convex/quizNode.tspackages/i18n/src/brand.tspackages/i18n/src/index.tspackages/i18n/src/strings.ts
💤 Files with no reviewable changes (3)
- apps/web/src/components/feed/event.tsx
- apps/web/src/components/SignInPrompt.tsx
- apps/web/src/routes/salvate.tsx
| { name: "twitter:description", content: description }, | ||
| { name: "twitter:image", content: SITE.ogImage }, | ||
| { name: "twitter:image:alt", content: title }, | ||
| { name: "theme-color", content: "#0f172a" }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Static theme-color doesn't adapt to the new light theme.
This PR adds a full light/dark theme system, but theme-color stays hardcoded to a single dark value (#0f172a), so the mobile browser chrome/PWA tab bar won't match light-mode pages. Browsers support per-scheme theme-color meta tags via media="(prefers-color-scheme: ...)".
💡 Proposed fix: split into light/dark theme-color tags
- { name: "theme-color", content: "`#0f172a`" },
+ { name: "theme-color", content: "`#ffffff`", media: "(prefers-color-scheme: light)" },
+ { name: "theme-color", content: "`#0f172a`", media: "(prefers-color-scheme: dark)" },Note this static meta approach only reflects OS-level prefers-color-scheme, not the app's explicit theme preference (light/dark/system) stored via ThemeProvider — for full parity you may also want to update theme-color dynamically from ThemeProvider when the user explicitly overrides the system preference.
📝 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.
| { name: "theme-color", content: "#0f172a" }, | |
| { name: "theme-color", content: "`#ffffff`", media: "(prefers-color-scheme: light)" }, | |
| { name: "theme-color", content: "`#0f172a`", media: "(prefers-color-scheme: dark)" }, |
🤖 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/__root.tsx` at line 113, The hardcoded theme-color meta
entry in __root.tsx only matches dark mode and won’t adapt for the new light
theme. Update the meta tags in the root route to provide separate light and dark
theme-color values using media="(prefers-color-scheme: ...)" so browser chrome
matches the active scheme. If the app’s ThemeProvider supports explicit
overrides beyond OS preference, also wire theme-color updates to that theme
state so the meta color stays in sync with user selection.
| // IPv6 loopback (::1), unspecified (::), link-local (fe80::) and | ||
| // unique-local (fc00::/7 → fc/fd prefixes). | ||
| if (host === "::1" || host === "::") return true; | ||
| if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
fc/fd prefix check blocks legitimate hostnames. The unique-local IPv6 guard uses bare startsWith("fc")/startsWith("fd"), which also matches ordinary DNS names — e.g. fda.gov, fc2.com, fcbarcelona.com — so any og:image/twitter:image hosted on such a domain is rejected as unreachable. Gate these prefixes on the host actually being an IPv6 literal (contains :).
🐛 Proposed fix
if (host === "::1" || host === "::") return true;
- if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) {
+ if (
+ host.includes(":") &&
+ (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd"))
+ ) {
return true;
}📝 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.
| // IPv6 loopback (::1), unspecified (::), link-local (fe80::) and | |
| // unique-local (fc00::/7 → fc/fd prefixes). | |
| if (host === "::1" || host === "::") return true; | |
| if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) { | |
| return true; | |
| } | |
| // IPv6 loopback (::1), unspecified (::), link-local (fe80::) and | |
| // unique-local (fc00::/7 → fc/fd prefixes). | |
| if (host === "::1" || host === "::") return true; | |
| if ( | |
| host.includes(":") && | |
| (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) | |
| ) { | |
| return true; | |
| } |
🤖 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/lib/imageVerification.ts` around lines 66 - 71, The
IPv6 unique-local guard in imageVerification logic is over-broad because the
host checks in the function that handles reachability treat any hostname
starting with “fc” or “fd” as blocked. Update the host validation so the
`fc`/`fd` prefixes are only applied when the value is clearly an IPv6 literal,
such as by requiring a colon in the host before matching those prefixes. Keep
the existing `::1`, `::`, and `fe80:` checks in the same helper, but narrow the
unique-local detection so ordinary DNS names like `fc2.com` or `fda.gov` are no
longer rejected.
Summary by CodeRabbit
New Features
Bug Fixes