The hardcoded 15-minute timeout was too short for some syntheses.
Now configurable via env var with a default of 30 minutes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that when a source page returns no article links (blocked/empty),
the pipeline does not crash and still produces article_history entries via
the site_search fallback path or Phase 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Build SiteSearchProvider before wave loop, chain as third fallback
after RSS + HTML when both return 0 links.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace placeholder search_llm with real implementation: builds a French
prompt asking the LLM for recent articles from a domain, calls call_llm
with a JSON-array schema, and filters results through url_matches_domain
to guard against hallucinated URLs. Add build_site_search_prompt and
parse_llm_url_response helpers with 4 unit tests (valid array, non-array,
mixed types, wrong-domain filtering).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6-task plan: site_search service (Brave + LLM paths), pipeline
integration as third fallback after RSS + HTML, tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spec for automatic site:{domain} search fallback when RSS + HTML
extraction both return 0 links for a personalized source. Uses
Brave Search or LLM websearch. Inline in Phase 1 spawn.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds chunked reading with a 5 MB cap (matching the scraper limit) to
both parse_feed and discover_feed, with fast rejection via Content-Length
header when available. Includes a unit test covering the oversize path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `AND user_id = $4` to the UPDATE query in `update_source_rss` and
threads the `user_id` parameter through from `run_generation_inner`,
consistent with every other mutation in db/sources.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds detect_and_parse_feed which orchestrates feed caching/freshness logic:
uses cached feed URL directly if fresh (< 30 days), otherwise re-discovers
from source URL via discover_feed. Returns FeedResult::Found or NotFound.
Includes 4 new tests covering fresh cache, no cache, no feed, and stale cache cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add nullable rss_url (TEXT) and rss_discovered_at (TIMESTAMPTZ) columns
to the sources table for RSS feed URL caching. Update the Source struct,
all query_as SELECT/RETURNING queries, and add update_source_rss db function.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7-task plan covering: feed-rs dependency, DB migration, feed_parser
service (parse, discover, orchestrate), pipeline integration, and tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spec for adding RSS/Atom feed support to personalized sources in Phase 1
of the synthesis pipeline — auto-discovery, persistence with 30-day
re-check, and fallback to HTML extraction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API client's 401 handler was intercepting responses from /auth/*
endpoints (login, register, me), throwing "Session expired" before the
actual response could reach the caller. This prevented the login form
from working — the AuthProvider's me() call returned 401, threw, and
the error propagated into the login flow.
Now the 401 redirect only triggers for non-auth API calls (where it
genuinely indicates an expired session). Auth endpoints handle their
own error responses normally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API client redirected to /login on any 401 response, including the
GET /auth/me call made by AuthProvider on the login page itself. This
caused an infinite hard-navigation reload loop.
Skip the redirect when already on /login or /register — the AuthContext
route guards handle unauthenticated routing for those pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CSP had connect-src 'self' which blocked Cloudflare Turnstile's
internal fetch requests to challenges.cloudflare.com, causing them to
hang indefinitely and triggering a page reload loop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ServeDir::not_found_service serves index.html but preserves the 404
status code. Switch to ServeDir::fallback which returns 200, fixing
client-side routes like /login returning 404 to the browser.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Turnstile tokens are single-use. The resend flow reused the consumed token,
causing "timeout-or-duplicate" errors from Cloudflare.
Frontend:
- Add Turnstile widget to resend view on Login and Register pages
- Add resetSignal prop to Turnstile component to re-solve after each resend
- Clear token after each successful API call, guard resend against null token
- Add test for resetSignal behavior
Backend:
- Add entry log when magic link email sending begins
- Add explicit warning when rate limit prevents sending
- Add error log with rollback context when email delivery fails
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Turnstile tokens are single-use. The resend flow was reusing the consumed
token from the initial submission, causing "timeout-or-duplicate" errors.
- Add Turnstile widget to the resend view so a fresh token is obtained
- Add resetSignal prop to Turnstile component to re-solve after each resend
- Clear token after each successful API call to prevent stale reuse
- Guard handleResend against null token
- Add test for resetSignal behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the Cloudflare Turnstile script fails to load (e.g., 503 from CDN),
the polling interval ran forever, causing the page to appear stuck in a
refresh loop. Now stops after 100 attempts (10s) and calls onError.
Also adds dedicated unit tests for the Turnstile component covering
immediate render, delayed load, timeout, and cleanup-during-polling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The frontend Vite build was not receiving VITE_TURNSTILE_SITE_KEY during
Docker builds, causing the production bundle to fall back to the Cloudflare
test sitekey (1x00000000000000000000AA) which returns 503 in production.
- Add ARG/ENV for VITE_TURNSTILE_SITE_KEY in Dockerfile frontend stage
(placed after npm ci to preserve dependency cache)
- Pass TURNSTILE_SITE_KEY from .env as build arg in docker-compose.yml
- Add post-change workflow section to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Host port changed from 8080 to 8005 and bound to 127.0.0.1 only
so traffic goes through Caddy (HTTPS) instead of being exposed directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Many modern sites (Hugo, WordPress, Next.js) load articles via JavaScript
but include full article URLs in JSON-LD schema.org markup in the <head>.
The scraper now extracts these first (highest quality), then falls back
to <a href> heuristic extraction. Supports ItemList, BlogPosting,
NewsArticle, @graph arrays, and mainEntity wrappers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Date parser now supports: 25/03/2026, 25-03-2026, March 25 2026,
25 mars 2026, 15 février 2026, and short month variants.
Articles without dates are no longer routed to a separate category —
they stay in their LLM-classified category with date shown as empty.
This prevents losing good articles in a catch-all section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Date aligned left, source URL aligned right. URL stripped of protocol
and truncated to 40 chars with "..." if too long. Full URL on hover.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API client expects empty responses to use 204, not 200.
Returning 200 with no body caused JSON parse error in the frontend.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract content settings card and sources card into dedicated components,
reducing ThemeManager from 938 to 233 lines while keeping theme list CRUD
and selector in the parent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace raw fetch in handleStop with synthesesApi.stop()
- Add stop() method to synthesesApi
- Replace raw <button> elements in GenerateSynthesis with Button component (generate, retry, stop)
- Remove deprecated LLM link extraction schema reference from technical_specs.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- export_csv now accepts optional theme_id query param and filters accordingly
- Add UpdateThemeRequest::validate() with bounds checking; call it in the update handler
- Verify theme ownership in sources::create when theme_id is provided
- Update STATUS_OPTIONS (add filtered_too_old, filtered_not_article; remove filtered_duplicate) and SOURCE_TYPE_OPTIONS (add brave_search; remove overflow) in ArticleHistory
- Replace hardcoded French strings ('Confirmer', 'Erreur inconnue') with t() calls; add settings.apiKeys.unknownError key to fr.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bulk/CSV import now passes theme_id through to DB
- Preferred source update scoped by theme_id (no cross-theme reset)
- Theme creation sends sensible defaults from frontend
- Scheduler wraps generation in 15-minute timeout
- Job store cleanup runs every 5 minutes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trim architecture.md significantly (section 1 overview, technology stack, deployment topology,
module inventory lists, LLM trait block, pipeline details, data model table, full API tables,
background task list). Replace section 5 API tables with a one-liner. Requirements.md sections
3.1/3.5/3.6/3.7/3.8 and 4.2 condensed with cross-references. deployment.md security feature
list replaced by cross-reference to architecture.md Section 6. functional_specs.md Section 3
gains a cross-reference to technical_specs.md Section 5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>