- CRITICAL: evaluateCriteria platform filter used AND logic (both checks fire independently) causing 100% flat rejection. Fixed to OR-logic allowedSources matching (matching list-flats.ts pattern). 4 new test cases. - Pipeline event logging: migration 00033 (pipeline_events table + indexes + cleanup), TypeScript types, pipelineRepo (logEvent, logEventWithTiming, getEvents), POST /api/webhooks/pipeline-log endpoint with SCRAPER_WEBHOOK_SECRET auth - scripts/backfill-evaluations.sh for re-evaluating all flats after platform fix - Fixed pre-existing test failures: added getServiceClient mock to stats test, unstable_cache mock to POI test - SessionInfoBar: Search Now always visible - Session token validation: migration 00032 - Agent configs, husky pre-commit hook, RLS migration |
||
|---|---|---|
| .agent_states | ||
| .husky | ||
| .opencode/agents | ||
| agents | ||
| docker | ||
| docs | ||
| e2e | ||
| public | ||
| scripts | ||
| src | ||
| supabase/migrations | ||
| templates | ||
| .agents.local.md | ||
| .artifacts.json | ||
| .dockerignore | ||
| .eslintrc.json | ||
| .gitignore | ||
| .project_rules.md | ||
| dev-proxy.mjs | ||
| eslint.config.mjs | ||
| gotchas.md | ||
| next-env.d.ts | ||
| next.config.mjs | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| pm_local_backup.md | ||
| postcss.config.mjs | ||
| README.md | ||
| requirements.txt | ||
| SESSION_STATE.md | ||
| TOOLS.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Flat-Hunter — Prague Property Command Center
Stack: Next.js 15.3 / React 19 / Tailwind 4 / Supabase (Postgres+PostGIS) / Mapbox GL JS / LangGraph
Runtime: Node 24, Python 3.12 (LangGraph agent on RPi 5)
Deploy target: Raspberry Pi 5 (ARM64, headless), containerized via Docker Compose
Repo: Private
What It Does
- Scrapes Czech rental flat listings from Sreality.cz and Bezrealitky.cz via Python agents
- Stores them in PostgreSQL with PostGIS geospatial support
- Presents listings in a Next.js web dashboard with dark-themed UI, map view, charts
- Evaluates flats against per-user search criteria (price range, layout, status)
- Machine translation of Czech descriptions to English via LangGraph + LLM
- Link verification, contact extraction (phone/email/agency) via LangGraph agents
- Points of Interest: proximity search for metro, tram, train stations, shopping malls (via PostGIS ST_Distance)
- Notifications via email (SMTP) and WhatsApp (Twilio)
- Flat sharing between sessions, flat notes, custom fees, CSV export
- Admin panel for scraper config, LLM provider management, monitoring
- Financial summaries (total cost, sunk costs, monthly run-rate)
- Session-based auth (no credentials needed — SHA256 hash = identity)
- Deployed to Raspberry Pi 5 with Caddy reverse proxy
Architecture Diagram
┌─────────────────────────────────────────────────────┐
│ Caddy (reverse proxy) │
│ :443 → :9000 (Next.js) :9001 → :3001 (PostgREST)│
└──────┬──────────────────────────┬───────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Next.js App │ │ PostgREST │
│ (Node 24) │ │ (port 3001) │
│ port 9000 │ └──────┬───────┘
└──────┬───────┘ │
│ │
│ ┌──────────────────────┤
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────┐
│ PostgreSQL 16 + PostGIS │
│ (Docker, port 5434) │
│ │
│ Tables: flats, users_sessions, │
│ scrape_queue, pois, notifications, │
│ flat_notes, scraper_config, ... │
└─────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Python Scheduler (Docker) │
│ Polls scrape_queue every 60s │
│ │ │
│ ├── scrape_sreality.py │
│ └── scrape_bezrealitky.py │
│ │ │
│ ▼ POST /api/webhooks │
│ ┌────────────────────┐ │
│ │ LangGraph Agents │ │
│ │ - link_verifier │ │
│ │ - contact_extractor│ │
│ │ - translator │ │
│ └────────────────────┘ │
└─────────────────────────────────────┘
Data Flow
Request → middleware (session gate)
→ route handler
→ withSession() (auth guard, session resolution, IP tracking)
→ application use-case
→ repository (Supabase client)
→ PostgreSQL / PostGIS
TypeScript Project Structure
src/
├── app/api/ # HTTP route handlers (~30 endpoints)
│ ├── admin/ # Admin panel API (separate auth)
│ ├── auth/ # Session login, password, token refresh
│ ├── flats/ # Flat CRUD, notes, fees, batch, export, stats
│ ├── notifications/ # User notification read/seen
│ ├── pois/ # Nearby Points of Interest
│ ├── scraper/ # Scraper trigger, status
│ ├── scraper-sessions/ # Admin scraper session listing
│ ├── shares/ # Flat sharing (invite, accept, reject)
│ └── webhooks/ # Inbound from Python agents (scraper, metrics)
├── application/ # Use-case orchestration layer
│ ├── auth/ # login, verify-session
│ ├── fees/ # add, delete, list fees
│ ├── flats/ # list, create, update, get, batch, export, stats, evaluate
│ └── scraper/ # get/update config, trigger scrape, status
├── domains/ # Pure types + business logic
│ ├── auth/types.ts # SessionHash, FlatStatus, SearchCriteria
│ ├── flat/types.ts # Flat entity (all fields)
│ ├── flat/financials.ts
│ ├── poi/types.ts
│ └── notification/types.ts
├── infrastructure/ # External service adapters
│ ├── supabase/repositories/ # session, flat, fee, contact, notification,
│ │ # scraper-config, scraper-session, admin, share
│ └── notifications/ # email-service (nodemailer), whatsapp-service (Twilio),
│ # dispatcher (routes to both)
├── lib/ # Cross-cutting utilities
│ ├── auth/ # session, session-tokens, session-password,
│ │ # admin-auth, timing-safe
│ └── supabase/client.ts
└── shared/ # Shared infrastructure
├── errors.ts # AppError, UnauthorizedError, NotFoundError, etc.
└── api/ # withSession() wrapper, getSessionHash()
Python Agents (agents/)
| File | Description |
|---|---|
scheduler.py |
Job scheduler — polls scrape_queue, spawns scrapers per-user with concurrency limit |
scrape_sreality.py |
Sreality.cz scraper — parses rental listings, posts to webhook |
scrape_bezrealitky.py |
Bezrealitky.cz scraper — fetches via JSON API |
flat_hunter_agent.py |
Main LangGraph runtime — reads LLM config from DB, runs inference |
link_verifier_agent.py |
Link verification — checks if listing URLs are still active |
contact_extractor_agent.py |
Contact extraction — regex + LLM phone/email/agency extraction |
translator_agent.py |
Czech→English translation via LLM |
seed_pois.py |
POI seeder — queries Overpass API for Prague transit stations |
Database Migrations (supabase/migrations/)
27 idempotent SQL migrations (00001–00027):
- Core schema: flats, users_sessions, pois (with PostGIS geometry)
- RPC functions: get_flat_stats, nearby_pois, list_flats_by_distance, ingest_flat
- Scraper: scraper_config, scrape_queue (per-user with cron/manual trigger types)
- Notifications: channels (email/WhatsApp), rules (critical/general)
- Multi-tenancy: session-aware filters, per-user dedup
- Admin: admin_users, admin_sessions
- Caching: stats and POI endpoints use
unstable_cachewith tag invalidation
Setup
Prerequisites
- Node.js 24+
- Python 3.12+
- Docker + Docker Compose
Local Development
-
Clone and install dependencies:
npm install pip install -r requirements.txt -
Configure environment:
cp .env.local.example .env.local # if example exists, otherwise create .env.localRequired:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEYOptional: SMTP/Twilio credentials for notifications,ADMIN_BACKDOOR_CODEfor e2e tests -
Start the database stack:
cd docker docker compose up -d db migrate rest -
Seed Points of Interest (one-time):
python3 agents/seed_pois.py -
Start the dev server:
npm run devOpens on
http://localhost:3000with the dev proxy forwarding Supabase requests to PostgREST on:3001. -
Start the scheduler (optional, for scraping):
cd docker docker compose up -d scheduler
Docker Production Deploy (Raspberry Pi 5)
# On the RPi, clone the repo and:
cd docker
ADMIN_BACKDOOR_CODE=your-secret docker compose up -d
Services: db (PostgreSQL), migrate (auto-runs on start), rest (PostgREST), web (Next.js, port 9000), scheduler (Python scraper loop).
Place a Caddy reverse proxy in front (see docker/Caddyfile):
flat.example.com→localhost:9000(Next.js)- Handles
/rest/v1/prefix stripping for PostgREST onlocalhost:3001
Environment Variables
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase/PostgREST endpoint |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Anon key for PostgREST |
SUPABASE_SERVICE_ROLE_KEY |
Admin | Service role key for privileged operations |
ADMIN_BACKDOOR_CODE |
E2E tests | Session hash that bypasses all auth checks |
DATABASE_URL |
Scheduler | PostgreSQL connection string for scheduler.py |
SMTP_HOST/PORT/USER/PASS/FROM |
Notifications | Email notification config |
TWILIO_ACCOUNT_SID/AUTH_TOKEN/WHATSAPP_FROM |
Notifications | WhatsApp notification config |
POSTGRES_PASSWORD |
Docker | DB password (default: flat_hunter_local_dev) |
SCHEDULER_POLL_SECONDS |
Scheduler | Poll interval (default: 60) |
MAX_CONCURRENT_SCRAPERS |
Scheduler | Max parallel scrapers (default: 2) |
SCRAPER_WEBHOOK_SECRET |
Scheduler+Web | Shared secret for scraper→webhook auth |
Available Scripts
| Script | Description |
|---|---|
npm run dev |
Start dev server (Next.js + proxy) |
npm run build |
Production build (includes lint + typecheck) |
npm start |
Start production server |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
npm test |
Vitest unit/integration tests |
npm run test:e2e |
Playwright end-to-end tests |
npm run migrate |
Apply Supabase migrations to local DB |
Key Design Decisions
Session Auth (zero-credential): Users are identified by SHA256(random-seed) = session_hash. No passwords, no OAuth. Sessions expire after 1 month unless marked is_paid. Admin access uses a separate cookie-based auth system.
Admin Backdoor: The ADMIN_BACKDOOR_CODE env var allows a known session hash to bypass all auth checks — used exclusively for e2e tests. All comparisons use constant-time timingSafeEqual().
Data Deduplication: Every incoming listing is deduplicated by source_url (URL-normalized) and composite_hash (MD5 of address + layout + price). Handled in the ingest_flat() RPC.
Caching: Stats (/api/flats/stats) and POI (/api/pois/nearby) endpoints use Next.js unstable_cache with tag-based invalidation. Flats mutation paths call revalidateTag('flat-stats') automatically.
Middleware (slim): The Edge middleware only checks session presence (redirects missing sessions). Heavy session resolution, validation, expiry checks, and IP tracking live in withSession(), executed in the Node.js runtime where Supabase access is available.
Repository Pattern: All database access goes through repository objects in src/infrastructure/supabase/repositories/. Route handlers never call Supabase directly (with controlled exceptions for admin routes).
Quality Gates: tsc --noEmit (0 errors required), ESLint (0 errors), Vitest, Playwright e2e. All gates must pass before commit.
Commit Convention: agent({agent_id}): {imperative verb} {summary}
Example: agent(coder_local): implement PostGIS nearest-POI RPC
Codebase Health
See docs/CODEBASE_AUDIT.md for the most recent full audit (C1–C9, priority-ordered).
See gotchas.md for accumulated failure log and prevention rules.
See TOOLS.md for custom tool configurations.