Skip to content

Romanian news - #39

Merged
flvvius merged 43 commits into
mainfrom
romanian-news
Jul 7, 2026
Merged

Romanian news#39
flvvius merged 43 commits into
mainfrom
romanian-news

Conversation

@flvvius

@flvvius flvvius commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added a unified theme system with light/dark/system support, plus theme controls in profile pages.
    • Introduced a richer feed and event experience, including quiz entry points, improved mobile navigation, and reading activity displays.
    • Added Romanian static footer/info pages with consistent layout and page metadata.
  • Bug Fixes

    • Improved mobile layout stability, scroll restoration, and tab bar behavior.
    • Fixed sign-in and social login error handling.
    • Updated Romanian copy and terminology across the app.

flvvius and others added 15 commits July 2, 2026 09:49
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>
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
news Error Error Jul 7, 2026 10:12am
news-web Ready Ready Preview, Comment Jul 7, 2026 10:12am

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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.

Changes

Bias axis rename

Layer / File(s) Summary
Spec docs and i18n copy
docs/bias-axis-spec.md, I18N_CHANGES.md, packages/i18n/src/strings.ts, apps/web/src/lib/i18n/strings.test.ts
Documents and renames the axis to reformist/suveranist across Romanian/English string catalogs.
Prompt builders and summarization
packages/backend/convex/prompts.ts, prompts.test.ts, summarizationNode.ts
Prompts and summary parsing now use neutral/reformist/suveranist keys and Romanian-only output rules.
Public preview normalization
lib/publicEventPreviews.ts, publicEventPreviews.test.ts
Normalizes perspective summaries and trending score signal fallback logic.
Data migrations
migrations.ts, singletonCleanup.ts
Backfills perspective/axis keys and centralizes event child-table cleanup.

Feature flags

Layer / File(s) Summary
Flag core & guard
feature-flags.ts, routes/quiz.tsx
Adds FEATURE_FLAGS and a route guard redirecting away from /quiz when disabled.
UI hiding & tests
header.tsx, MobileTabBar.tsx, quiz-cta.tsx, quiz-hidden.test.tsx
Filters quiz links/tabs and verifies no entry points render.

Theme system

Layer / File(s) Summary
Core utilities
lib/theme/index.ts, tests
Theme preference storage/resolution and no-flash script.
Provider & components
ThemeProvider.tsx, ThemePicker.tsx, ThemeToggle.tsx
Theme context and picker/toggle UI.
Wiring
AnonymousProfile.tsx, AuthenticatedProfile.tsx, __root.tsx, index.css
Integrates theme into profile pages and root document/CSS tokens.
E2E & overlay styling
e2e/theme.spec.ts, button.tsx, drawer.tsx, sheet.tsx
Theme persistence test and overlay token updates.

Mobile layout & feed redesign

Layer / File(s) Summary
Safe-area foundations
Footer.tsx, BrandLogo.tsx, mobile e2e specs
Safe-area padding and pinned tab-bar tests.
EventCard editorial row
event-card.tsx, articles-list.tsx, source-coverage-summary.tsx, section-title.tsx
Redesigns cards/lists as editorial rows.
Feed & saved page
feed.tsx, salvate.tsx, SignInPrompt.tsx, sign-in/up forms
Simplifies feed header and saved-page UI.
Event detail redesign
event.$slug.tsx, event-detail-tabs.tsx, source.$sourceId.tsx
Hero layout and tabbed perspective/claims content.

Static pages & branding

Layer / File(s) Summary
Footer pages
StaticPage.tsx, footer routes, FOOTER_TODO.md
Static Romanian content for legal/info pages.
BRAND_NAME plumbing
packages/i18n/src/brand.ts, backend/convex/brand.ts, seo.ts, emails.ts
Centralized brand name used in SEO, emails, and native app.

Ingestion & topic catalog

Layer / File(s) Summary
Romanian text utils
lib/romanian.ts
Diacritics normalization, folding, pluralization.
Feed catalog & reputation
feeds.ts, sourceReputation.ts, seeds.ts
Romanian tiered feed catalog seeded from manual reputation data.
Ingestion pipeline
ingestion.ts
Feed quarantine and Google News overlay ingestion.
Topic catalog & clustering
topicCatalog.ts, topics.ts, clustering.ts
Romanian topic taxonomy and admin backfill pipeline.

Image pipeline

Layer / File(s) Summary
Sniffing & verification
lib/imageSniff.ts, lib/imageVerification.ts
Byte-level format detection and SSRF-safe URL verification.
Article extraction
lib/articleExtraction.ts, shareAssetsNode.ts
Body/image extraction rework and Resvg-safe share assets.

Auth & model routing

Layer / File(s) Summary
Session healing
auth.ts, lib/userProfile.ts, auth-social.tsx
Best-effort profile healing and fixed sign-in error handling.
Model routing
config.ts, enrichmentNode.ts, quizNode.ts
Switches to shared LLM helper and default models.

Activity dashboard

Layer / File(s) Summary
Reading calendar
current-month-reading-calendar.tsx, recent-reading-item.tsx, activitate.tsx
Replaces streak calendar with monthly reading view.
Dashboard query
interactions.ts
UTC-based reading calendar computation.

Test infra

Layer / File(s) Summary
Scroll restoration & feed query
router.tsx, feed-query.ts
Router scroll workaround and query arg helper.
Tooling config
playwright.config.ts, vitest.config.ts, package.json
Test tooling setup.

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
Loading
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
Loading

Possibly related PRs

  • flvvius/news#5: Both PRs modify EventCard's bookmark rendering behavior (showBookmark prop and placement outside the link).
  • flvvius/news#22: The main PR's frontend consumption of neutral/reformist/suveranist perspective fields aligns with this PR's backend clustering changes preserving those same fields.
  • flvvius/news#15: Both PRs modify routes/event.$slug.tsx's perspective tabs/summary layout.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is very generic and doesn’t clearly describe the main changes in the pull request. Use a concise, specific title that highlights the primary change, such as Romanian localization and news UI/content updates.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch romanian-news

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

flvvius and others added 12 commits July 2, 2026 16:00
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>
flvvius and others added 3 commits July 4, 2026 13:41
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>
flvvius and others added 3 commits July 4, 2026 17:20
…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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 win

Remove the unused duplicate EventCard apps/web/src/components/feed/event.tsx isn’t imported anywhere; the feed routes use apps/web/src/components/feed/event-card.tsx instead. 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 win

Extract shared source-insert payload to remove duplication.

createSource and getOrCreateSource now share an identical args shape and an identical ctx.db.insert("sources", {...}) payload (domain, name, bias via namedAxisBias(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 createSource and getOrCreateSource handlers call ctx.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

📥 Commits

Reviewing files that changed from the base of the PR and between db11806 and 4a68bb3.

⛔ Files ignored due to path filters (3)
  • apps/native/ios/Podfile.lock is excluded by !**/*.lock
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**, !**/_generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (90)
  • FOOTER_TODO.md
  • I18N_CHANGES.md
  • README.md
  • apps/web/.gitignore
  • apps/web/e2e/helpers.ts
  • apps/web/e2e/horizontal-overflow.spec.ts
  • apps/web/e2e/mobile-tab-bar.spec.ts
  • apps/web/e2e/scroll-restoration.spec.ts
  • apps/web/package.json
  • apps/web/playwright.config.ts
  • apps/web/src/components/SignInPrompt.tsx
  • apps/web/src/components/auth-prompt-banner.tsx
  • apps/web/src/components/auth-social.test.tsx
  • apps/web/src/components/auth-social.tsx
  • apps/web/src/components/feed/articles-list.tsx
  • apps/web/src/components/feed/event-card.test.tsx
  • apps/web/src/components/feed/event-card.tsx
  • apps/web/src/components/feed/event-detail-tabs.test.tsx
  • apps/web/src/components/feed/event-detail-tabs.tsx
  • apps/web/src/components/feed/event.tsx
  • apps/web/src/components/feed/source-coverage-summary.tsx
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/Footer.tsx
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/components/layout/StaticPage.tsx
  • apps/web/src/components/quiz-cta.tsx
  • apps/web/src/components/quiz-hidden.test.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/sign-up-form.tsx
  • apps/web/src/components/ui/button.tsx
  • apps/web/src/components/ui/drawer.tsx
  • apps/web/src/components/ui/sheet.tsx
  • apps/web/src/design-system.test.ts
  • apps/web/src/index.css
  • apps/web/src/lib/feature-flags.ts
  • apps/web/src/lib/feed-query.test.ts
  • apps/web/src/lib/feed-query.ts
  • apps/web/src/lib/i18n/strings.test.ts
  • apps/web/src/router.tsx
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/contact.tsx
  • apps/web/src/routes/cum-functioneaza.tsx
  • apps/web/src/routes/despre.tsx
  • apps/web/src/routes/event.$slug.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/footer-pages.test.tsx
  • apps/web/src/routes/parteneri.tsx
  • apps/web/src/routes/politica-confidentialitate.tsx
  • apps/web/src/routes/quiz.tsx
  • apps/web/src/routes/salvate.tsx
  • apps/web/src/routes/source.$sourceId.tsx
  • apps/web/src/routes/sursele-noastre.tsx
  • apps/web/src/routes/termeni.tsx
  • apps/web/vitest.config.ts
  • docs/bias-axis-spec.md
  • docs/source-balance-biv806.md
  • packages/backend/convex/articleExtraction.test.ts
  • packages/backend/convex/auth.test.ts
  • packages/backend/convex/auth.ts
  • packages/backend/convex/clustering.ts
  • packages/backend/convex/enrichmentNode.ts
  • packages/backend/convex/feeds.smoke.test.ts
  • packages/backend/convex/feeds.test.ts
  • packages/backend/convex/feeds.ts
  • packages/backend/convex/imageSniff.test.ts
  • packages/backend/convex/imageVerification.test.ts
  • packages/backend/convex/ingestion.ts
  • packages/backend/convex/lib/articleExtraction.ts
  • packages/backend/convex/lib/imageSniff.ts
  • packages/backend/convex/lib/imageVerification.ts
  • packages/backend/convex/lib/publicEventPreviews.ts
  • packages/backend/convex/lib/romanian.ts
  • packages/backend/convex/lib/userProfile.ts
  • packages/backend/convex/migrations.ts
  • packages/backend/convex/prompts.test.ts
  • packages/backend/convex/prompts.ts
  • packages/backend/convex/publicEventPreviews.test.ts
  • packages/backend/convex/romanian.test.ts
  • packages/backend/convex/seeds.ts
  • packages/backend/convex/shareAssetsNode.ts
  • packages/backend/convex/singletonCleanup.ts
  • packages/backend/convex/sourceReputation.test.ts
  • packages/backend/convex/sourceReputation.ts
  • packages/backend/convex/summarizationNode.ts
  • packages/backend/convex/topicCatalog.test.ts
  • packages/backend/convex/topicCatalog.ts
  • packages/backend/convex/topics.ts
  • packages/backend/package.json
  • packages/i18n/src/strings.ts

Comment on lines +65 to +81
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 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.

Comment thread apps/web/src/components/feed/event-detail-tabs.tsx
Comment thread apps/web/src/components/header.tsx
Comment thread apps/web/src/components/quiz-cta.tsx
Comment thread apps/web/src/components/SignInPrompt.tsx Outdated
Comment thread packages/backend/convex/lib/imageVerification.ts
Comment thread packages/backend/convex/migrations.ts Outdated
Comment on lines +128 to +143
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;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 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

Comment thread packages/i18n/src/strings.ts
Comment thread packages/i18n/src/strings.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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 win

Generic label/text loses per-row distinguishability in a list.

Swapping article.event.title for a fixed t("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 plus article.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 win

Drop the unused maxSources prop from EventCardProps. 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 win

Restore a fallback for unclustered articles. When article.event is 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 win

Duplicate card markup — reuse RecentReadingItem instead 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 (bookmarkedAt vs lastViewedAt) and the fallback icon (Bookmark vs Newspaper). Generalizing the extracted component (e.g. accept timestamp/fallbackIcon props) 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 win

Retry profile-heal failures instead of only logging them
A transient failure here still leaves legacy users without users/userStats until 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4a68bb3 and 5dd5b17.

⛔ Files ignored due to path filters (1)
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**, !**/_generated/**
📒 Files selected for processing (55)
  • apps/native/app/(tabs)/index.tsx
  • apps/native/app/source/[sourceId].tsx
  • apps/web/e2e/theme.spec.ts
  • apps/web/src/components/activity/recent-reading-item.test.tsx
  • apps/web/src/components/activity/recent-reading-item.tsx
  • apps/web/src/components/current-month-reading-calendar.test.tsx
  • apps/web/src/components/current-month-reading-calendar.tsx
  • apps/web/src/components/feed/articles-list.tsx
  • apps/web/src/components/feed/event-card.test.tsx
  • apps/web/src/components/feed/event-card.tsx
  • apps/web/src/components/feed/event-claim-comparison.tsx
  • apps/web/src/components/feed/event-detail-tabs.tsx
  • apps/web/src/components/feed/source-coverage-summary.tsx
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/Footer.tsx
  • apps/web/src/components/profile/AnonymousProfile.tsx
  • apps/web/src/components/profile/AuthenticatedProfile.tsx
  • apps/web/src/components/streak-activity-calendar.tsx
  • apps/web/src/components/theme/ThemePicker.tsx
  • apps/web/src/components/ui/section-title.test.tsx
  • apps/web/src/components/ui/section-title.tsx
  • apps/web/src/design-system.test.ts
  • apps/web/src/index.css
  • apps/web/src/lib/i18n/strings.ts
  • apps/web/src/lib/seo.ts
  • apps/web/src/lib/theme/ThemeProvider.test.tsx
  • apps/web/src/lib/theme/ThemeProvider.tsx
  • apps/web/src/lib/theme/index.test.ts
  • apps/web/src/lib/theme/index.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/contact.tsx
  • apps/web/src/routes/cum-functioneaza.tsx
  • apps/web/src/routes/despre.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/footer-pages.test.tsx
  • apps/web/src/routes/parteneri.tsx
  • apps/web/src/routes/politica-confidentialitate.tsx
  • apps/web/src/routes/quiz.tsx
  • apps/web/src/routes/salvate.tsx
  • apps/web/src/routes/source.$sourceId.tsx
  • apps/web/src/routes/sursele-noastre.tsx
  • apps/web/src/routes/termeni.tsx
  • apps/web/src/routes/unsubscribe.tsx
  • docs/code-audit-2026-07.md
  • packages/backend/convex/auth.ts
  • packages/backend/convex/brand.ts
  • packages/backend/convex/config.ts
  • packages/backend/convex/emails.ts
  • packages/backend/convex/interactions.ts
  • packages/backend/convex/prompts.ts
  • packages/backend/convex/quizNode.ts
  • packages/i18n/src/brand.ts
  • packages/i18n/src/index.ts
  • packages/i18n/src/strings.ts
💤 Files with no reviewable changes (1)
  • apps/web/src/components/streak-activity-calendar.tsx

Comment on lines +60 to +80
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 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.

Suggested change
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.

Comment thread docs/code-audit-2026-07.md Outdated
Comment on lines +16 to +24
**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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 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.

@flvvius
flvvius merged commit dfb215b into main Jul 7, 2026
2 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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 win

Feature 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 win

Use spaces in the calc() value. pb-[calc(5rem+var(--safe-area-bottom))] is emitted as an invalid calc(), so the mobile bottom padding is dropped. Change it to pb-[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 win

Normalize 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 win

Dangling 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 win

Generic 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 used article.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

📥 Commits

Reviewing files that changed from the base of the PR and between 5dd5b17 and 9de4cec.

⛔ Files ignored due to path filters (6)
  • apps/web/public/logo-biviant-dark.png is excluded by !**/*.png
  • apps/web/public/logo-biviant.png is excluded by !**/*.png
  • apps/web/public/logo-mark.png is excluded by !**/*.png
  • apps/web/public/og-image.jpg is excluded by !**/*.jpg
  • apps/web/public/og-image.png is excluded by !**/*.png
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**, !**/_generated/**
📒 Files selected for processing (42)
  • apps/native/app/(tabs)/index.tsx
  • apps/native/app/source/[sourceId].tsx
  • apps/web/src/components/BrandLogo.tsx
  • apps/web/src/components/SignInPrompt.tsx
  • apps/web/src/components/feed/event-card.tsx
  • apps/web/src/components/feed/event-claim-comparison.tsx
  • apps/web/src/components/feed/event-detail-tabs.tsx
  • apps/web/src/components/feed/event.tsx
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/Footer.tsx
  • apps/web/src/components/quiz-cta.tsx
  • apps/web/src/components/theme/ThemeToggle.tsx
  • apps/web/src/lib/i18n/strings.ts
  • apps/web/src/lib/seo.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/contact.tsx
  • apps/web/src/routes/cum-functioneaza.tsx
  • apps/web/src/routes/despre.tsx
  • apps/web/src/routes/footer-pages.test.tsx
  • apps/web/src/routes/parteneri.tsx
  • apps/web/src/routes/politica-confidentialitate.tsx
  • apps/web/src/routes/quiz.tsx
  • apps/web/src/routes/salvate.tsx
  • apps/web/src/routes/source.$sourceId.tsx
  • apps/web/src/routes/sursele-noastre.tsx
  • apps/web/src/routes/termeni.tsx
  • apps/web/src/routes/unsubscribe.tsx
  • packages/backend/convex/auth.ts
  • packages/backend/convex/brand.ts
  • packages/backend/convex/config.ts
  • packages/backend/convex/emails.ts
  • packages/backend/convex/enrichmentNode.ts
  • packages/backend/convex/imageVerification.test.ts
  • packages/backend/convex/ingestion.ts
  • packages/backend/convex/lib/articleExtraction.ts
  • packages/backend/convex/lib/imageVerification.ts
  • packages/backend/convex/migrations.ts
  • packages/backend/convex/prompts.ts
  • packages/backend/convex/quizNode.ts
  • packages/i18n/src/brand.ts
  • packages/i18n/src/index.ts
  • packages/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" },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
{ 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.

Comment on lines +66 to +71
// 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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
// 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant