You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AI-assisted RSS feed generator. Provide a target website URL, and the system automatically crawls pages, uses AI to infer CSS selectors for list and article content, extracts structured articles, and serves standard RSS feeds.
Features
Two-Stage RSS Extraction
List page analysis: AI infers CSS selectors for article links, titles, and dates from the listing page HTML (list_rules)
Content page analysis: AI infers selectors for article body, author, and images from individual article pages (content_rules)
Each stage is independently configurable — users can manually refine AI-suggested selectors via the Analyze page
AI-Powered CSS Selector Inference
Sends structurally-cleaned HTML to an LLM for selector inference; clean_html_for_ai strips noise separately for list vs. content mode
Supports OpenAI, Anthropic, Gemini, and any OpenAI-compatible endpoint — configured per user
Interactive Analyze page lets users test selectors and iterate without re-crawling
Lazy-Load Image Resolution
Modern sites use src="/placeholder.png" with real URLs in data-original, data-src, data-lazy-src, or data-lazy attributes — standard parsers miss these
_resolve_lazy_images() resolves the fallback chain and is applied at three pipeline stages: content sanitization, AI HTML cleaning (list and content modes), and hero image extraction
feedparser's HTML sanitizer is patched to preserve lazy-load attributes so they survive the sanitization pass
HTML Sanitizer Pipeline
sanitize_content_html: strict tag whitelist (p, span, a, img, ul, ol, li, h2–h6, figure, code, strong, em) produces clean RSS output
clean_html_for_ai: reduces full-page HTML to the relevant container before sending to the LLM, with separate list and content modes
Vue.js detection: decode_vue_gallery decodes x-data component JSON into standard HTML; extract_template_html handles Vue template structures
Multi-Provider AI with Ordered Fallback
Per-user provider profiles with drag-to-reorder priority; providers can be individually enabled or disabled
Fallback engine: works through the priority chain under a shared total deadline, classifying failures progressively
Model discovery and connection test built into the management UI; API key reveal is password-gated
SSRF protection: DNS resolution validation blocks private/LAN/cloud-metadata IPs for custom endpoints
KEK Envelope Encryption
API keys encrypted at rest using KEK/DEK envelope encryption — the KEK never touches the database
AAD binding (user, provider, protocol, base URL, version) prevents credential-swapping attacks even with database access
KEK is auto-generated on first startup; stored in a Docker volume or .dev-secrets/ — no manual key setup
Fail-closed: if the KEK is unavailable and providers are configured, the application refuses to start
Crawl Auto-Repair
RepairOrchestrator: consecutive crawl failures automatically trigger AI re-analysis of the feed's source page
Full flow: failure counting → weekly budget check → AI re-analyze → evidence-based candidate validation → atomic rule promotion
Candidate rules are validated by re-parsing the live HTML before any promotion — old rules are preserved on any failure path
AI calls are never held inside a database transaction
Article Filter Engine
Recursive tri-state rule evaluation supporting both blacklist and whitelist modes
Field targeting: filter on title, content, or both; regex supported for pattern matching
Configured per-feed via the edit page — no global filter that affects all feeds
Crawl Infrastructure
Multi-method fetching: Scrapling as the default with Playwright (headless Chromium) as an automatic fallback
asyncio.gather + Semaphore(3) for parallel article fetching; browser instances are reused within a single crawl run
APScheduler with per-site refresh intervals and random jitter to prevent thundering-herd effects on shared origins
Debug output system captures per-stage intermediate artifacts for troubleshooting extraction issues
Screenshots
Dashboard — feed overview and system status
Analytics — article growth charts and statistics
Add Feed — two-stage AI analysis with List/Content rules
AI Service — multi-provider management with fallback chain
Articles — full article list with search and filtering
Manage Feeds — feed list with actions
Quick Start With Docker
Docker Compose Example
Create a docker-compose.yml file:
services:
db:
image: postgres:17-alpinecontainer_name: palimpsest-dbenvironment:
POSTGRES_USER: palimpsestPOSTGRES_PASSWORD: palimpsestPOSTGRES_DB: palimpsestvolumes:
- ./data/postgres:/var/lib/postgresql/datahealthcheck:
test: ["CMD-SHELL", "pg_isready -U palimpsest -d palimpsest"]interval: 10stimeout: 5sretries: 5networks:
- palimpsest-networkchrome:
image: browserless/chrome:latestcontainer_name: palimpsest-chromeenvironment:
MAX_CONCURRENT_SESSIONS: 10CONNECTION_TIMEOUT: 300000ports:
- "3000:3000"networks:
- palimpsest-networkapp:
image: jhangyu/palimpsest:latestcontainer_name: palimpsest-appenvironment:
DATABASE_URL: postgresql://palimpsest:palimpsest@db:5432/palimpsest# Browserless Chrome connectionCHROME_MODE: serverCHROME_WS_ENDPOINT: ws://chrome:3000BROWSER_WS_URL: ws://chrome:3000POSTGRES_HOST: dbPOSTGRES_PORT: 5432CHROME_HOST: chromeCHROME_PORT: 3000# App settingsBACKEND_PORT: 8088PALIMPSEST_DATA_DIR: /app/dataPALIMPSEST_LOG_DIR: /app/log# KEK (Key Encryption Key) — auto-generated on first startup, no manual setup neededLLM_KEK_PATH: /app/data/kekLLM_KEK_VERSION: v1# LLM provider settingsLLM_PROVIDER_PROFILES_ENABLED: "true"LLM_FALLBACK_ENABLED: "true"LLM_FALLBACK_PROTOCOL: openaiLLM_FALLBACK_MAX_TOKENS: "4096"LLM_FALLBACK_THINKING: "false"LLM_FALLBACK_EFFORT: low# Uncomment and fill in to use a fallback LLM provider at startup:# LLM_FALLBACK_BASE_URL: https://api.openai.com/v1# LLM_FALLBACK_API_KEY: sk-your-key-here# LLM_FALLBACK_MODEL: gpt-4o# Auth settingsSESSION_COOKIE_SECURE: "false"SESSION_TTL_HOURS: "24"AUTH_ALLOW_PUBLIC_REGISTRATION: "false"AUTH_DEV_EXPOSE_RESET_LINK: "true"# CORS (leave empty to allow all origins, or set your frontend URL)ALLOWED_ORIGINS: ""FRONTEND_ORIGIN: ""volumes:
- ./data/app:/app/data
- ./log:/app/logports:
- "8088:8088"depends_on:
db:
condition: service_healthychrome:
condition: service_startednetworks:
- palimpsest-networkhealthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8088/health', timeout=5).read()"]interval: 30stimeout: 10sretries: 3start_period: 10snetworks:
palimpsest-network:
driver: bridge
Start the Stack
docker compose up -d
First-Run Setup
Open http://localhost:8088 in your browser. When no users exist, the app automatically redirects to the first-run setup page where you can create the admin account.
The KEK (Key Encryption Key) keyring is auto-generated on first startup — no manual key file creation or keyring directory setup is needed.
Configure AI Provider (Optional)
After logging in, go to Settings → AI Service to add LLM providers (OpenAI, Anthropic, Gemini, or any OpenAI-compatible endpoint).
Alternatively, set the LLM_FALLBACK_* environment variables directly in docker-compose.yml before starting the stack:
# Backendcd backend
python -m uvicorn main:app --host 0.0.0.0 --port 8088
# Astro (in another terminal)cd frontend-astro
npm install
npm run dev -- --host 0.0.0.0 --port 5174
Note: in development mode, the frontend sends API requests directly to http://localhost:8088. The backend must allow this origin in ALLOWED_ORIGINS.
The CSS pipeline runs via tools/dev.mjs alongside the Astro dev server. If SCSS changes are not reflected, ensure the dev server was started with tools/dev.mjs (not astro dev directly).
Testing
Backend Tests
Requires a running PostgreSQL test database. The conftest creates test tables per session.
# Set PYTHONPATH for dual import stylesexport PYTHONPATH="$(pwd):$(pwd)/backend:$(pwd)/tests"# Run all backend tests
python -m pytest tests/ -v
# Run specific test suites
python -m pytest tests/stage1/test_auth.py -v
python -m pytest tests/stage1/test_crawl_repair_service.py -v
python -m pytest tests/stage1/test_llm_provider_crud.py -v
python -m pytest tests/stage1/test_llm_fallback.py -v
Frontend E2E Tests
Playwright E2E test suite (434 tests across 11 spec files):
cd frontend-astro
npm install
npx playwright test
Architecture Decisions
PostgreSQL-only: SQLite support has been removed. DATABASE_URL must point to PostgreSQL. PostgreSQL 17 is required.
SQLAlchemy 2.0 async: All database access uses the SQLAlchemy 2.0 AsyncSession API and Core expressions. Raw SQL has been eliminated from the application layer.
Browser strategy: Service mode (Browserless Chrome at ws://chrome:3000) in Docker; local mode (CHROME_MODE=local) for dev. Scrapling is the default fetcher; Playwright is used as a fallback. Browser connections are reused within a crawl run.
Crawl auto-repair: When a site accumulates consecutive failures, RepairOrchestrator triggers an AI re-analysis cycle. Candidate rules undergo evidence-based validation before atomic promotion. Old rules are preserved on any failure path. AI calls are never held inside a DB transaction. A weekly budget cap prevents runaway repair attempts.
Scheduler: APScheduler AsyncIOScheduler runs every hour with jitter=300 (random 0-300 second stagger). Per-site refresh_frequency controls interval.
Reverse proxy: Nginx Proxy Manager -> Astro (port 5174) -> Vite proxy -> Backend (port 8088). In production, API_BASE = "" (relative path). In dev, API calls go directly to localhost:8088.
Frontend JS: Two independent pipelines: src/js/ (Rollup IIFE for layout utilities) and src/scripts/ (Astro Vite for business logic). They must not import from each other. Cross-pipeline communication uses CustomEvent.
CSS pipeline: SCSS -> sass-embedded + PostCSS + LightningCSS, independent of Vite. Dev mode auto-watches via tools/dev.mjs.
CJK word count: Shared compute_visible_word_count() helper. English words counted by token, CJK by character. Used in crawler inserts, updates, and backfill to ensure consistency.
Timezone: DB stores timezone-aware UTC. API bucketing converts to Asia/Taipei before grouping.
KEK auto-generation: The KEK is auto-generated on first startup if not present. The app tries multiple candidate paths (PALIMPSEST_DATA_DIR, /run/secrets) and generates a key file at the first writable location. Manual keyring setup is no longer required. If the KEK is unavailable at startup and providers exist, the app still refuses to start (fail-closed).
Feed parser: feed_parser.py integrates feedparser for RSS/Atom feed ingestion. feedparser's acceptable_attributes is extended to preserve lazy-load attributes (data-src, data-lazy, loading, etc.) during content sanitization.
Lazy-load resolution: _resolve_lazy_images() is a shared helper applied at three pipeline points to normalize lazy-loaded images into standard src attributes before storage and delivery.
Docker image: The published image is tagged latest (jhangyu/palimpsest:latest). The Dockerfile build label reflects the current release version.
Security
API keys: Never commit real API keys, KEK key material, or .env files. Use .env.example as the public template.
Provider credentials: Encrypted at rest using KEK/DEK envelope encryption with AAD binding (user, provider, protocol, base URL, version). Ciphertext is bound to its connection context to prevent credential swapping attacks.
SSRF protection: Custom provider base URLs are DNS-resolved and validated. Loopback, private, link-local, and metadata IPs are rejected unless explicitly allowlisted.
Session cookies: HttpOnly, SameSite=Lax, server-side validated. CSRF double-submit cookie on all state-changing endpoints.
Password hashing: Argon2id with automatic in-place rehash on login when parameters change.
Sensitive endpoints: Rate-limited (login, password reset, email verification). Admin-only database export and import endpoints are access-controlled.
Exposure: If an API key or credential has ever appeared in a log, screenshot, or commit, rotate it before publishing the repository.
Development Notes
Development process and conventions are documented in rule.md. See maintenance_docs_guide.md for the document maintenance policy.
All generated and local-only directories (data/, log/, .dev-secrets/, node_modules/, .venv/, dist/) are gitignored.
Use Conventional Commits when committing changes.
The docs/ directory contains AI development notes and task logs; these are local reference material, not published documentation.
About
AI-assisted Full-text RSS feed generator with Astro, Scrapling, Playwright, and Docker