Combined flat-alert + flat-apply with authenticated web UI
Find a file
Moritz 7444f90d6a per-step screenshot + html snapshots, matches-only list, full German UI, CSV export
* apply: Recorder.step_snap(page, name) captures both a JPEG screenshot and
  the page HTML for every major moment; every provider now calls step_snap at
  each logical step so failure reports contain the exact DOM and rendered
  state at every stage of the flow
* ZIP report: each snapshot becomes snapshots/NN_<label>.jpg +
  snapshots/NN_<label>.html for AI-assisted debugging
* web: Wohnungsliste zeigt nur noch Flats, die die eigenen Filter treffen;
  Match-Chip entfernt (Liste ist jetzt implizit matchend)
* UI komplett auf Deutsch: Protokoll statt Logs, Administrator statt admin,
  Trockenmodus statt dry-run, Automatik pausiert statt circuit open,
  Alarm statt Alert, Abmelden statt Logout
* Wohnungen-Header: Zeile 1 Info (Alarm + Filter), Zeile 2 Schalter mit
  echten Radio-Paaren (An/Aus) für Automatisch bewerben und Trockenmodus;
  hx-confirm auf den kritischen Radios; per-form CSS für sichtbaren Check-State
* Protokoll: von/bis-Datumsfilter (Berliner Zeit) + CSV-Download
  (/logs/export.csv) mit UTC + lokaler Zeit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:40:12 +02:00
alert lazyflat: combined alert + apply behind authenticated web UI 2026-04-21 09:51:35 +02:00
apply per-step screenshot + html snapshots, matches-only list, full German UI, CSV export 2026-04-21 11:40:12 +02:00
web per-step screenshot + html snapshots, matches-only list, full German UI, CSV export 2026-04-21 11:40:12 +02:00
.env.example lazyflat: combined alert + apply behind authenticated web UI 2026-04-21 09:51:35 +02:00
.gitignore lazyflat: combined alert + apply behind authenticated web UI 2026-04-21 09:51:35 +02:00
docker-compose.yml multi-user: users, per-user profiles/filters/notifications, tab UI, apply forensics 2026-04-21 10:52:41 +02:00
README.md lazyflat: combined alert + apply behind authenticated web UI 2026-04-21 09:51:35 +02:00

lazyflat

Combined deployment of flat-alert (reliable scraper) and flat-apply (experimental autoapplier) behind a single authenticated web UI.

Architecture

Three isolated containers on one internal Docker network:

┌─────────┐   POST /internal/flats    ┌─────────┐   POST /apply     ┌─────────┐
│  alert  │ ────────────────────────► │   web   │ ────────────────► │  apply  │
│ scraper │                           │  UI+DB  │                   │ browser │
└─────────┘                           └─────────┘                   └─────────┘
                                          ▲
                                          │  HTTPS (Coolify / Traefik)
                                          │
                                       user (auth)
  • alert/ — scrapes inberlinwohnen.de (unchanged logic) and posts each discovered flat to web. Zero external dependencies on apply, so apply crashes can never bring the alerter down.
  • apply/ — FastAPI wrapper around the experimental Playwright applier. Only accepts requests with a shared X-Internal-Api-Key. Not exposed publicly.
  • web/ — FastAPI + Jinja + HTMX dashboard. The only public service. Owns the SQLite database, auth, and orchestration.

Safety / isolation

Because apply/ is still experimental, the system is hardened around it:

Control Behavior
Separate container A crashed apply does not take alert or web with it.
Internal-only network apply is not reachable from the internet; requires internal API key.
Default mode manual New flats are just shown in the UI; apply runs only on click.
Circuit breaker N consecutive apply failures auto-disable further apply calls.
Kill switch One-click button in the UI that blocks all apply activity.
SUBMIT_FORMS=False Default for apply — runs the full flow without final submit.
Audit log Every auth event, mode change, and apply is recorded.

Web security

  • Argon2id password hashes (argon2-cffi), constant-time compare.
  • Session cookie: signed with itsdangerous, HttpOnly, Secure, SameSite=Strict.
  • CSRF: synchronizer token bound to the session on every state-changing form.
  • Login rate limit (in-memory, per-IP).
  • Strict Content-Security-Policy, X-Frame-Options: DENY, noindex.
  • /internal/* endpoints gated by a shared INTERNAL_API_KEY and never exposed by Coolify.

Deployment on Coolify

  1. Create repo: push this monorepo to ssh://git@git.moritz.run:2222/moritz/lazyflat.git.
  2. New Coolify resourceDocker Compose → point it at this repo. Coolify will read docker-compose.yml and deploy all three services on one network.
  3. Domain: set flat.lab.moritz.run on the web service only. Coolify (Traefik) handles TLS. Do not set a domain on alert or apply.
  4. Secrets: paste the environment variables from .env.example into Coolify's env UI. At minimum you need:
    • AUTH_USERNAME, AUTH_PASSWORD_HASH
    • SESSION_SECRET, INTERNAL_API_KEY
    • GMAPS_API_KEY, BERLIN_WOHNEN_USERNAME, BERLIN_WOHNEN_PASSWORD
    • personal info + WBS for apply
  5. Generate the password hash:
    python -c "from argon2 import PasswordHasher; print(PasswordHasher().hash('<your-password>'))"
    
  6. Generate secrets:
    python -c "import secrets; print(secrets.token_urlsafe(48))"   # SESSION_SECRET
    python -c "import secrets; print(secrets.token_urlsafe(48))"   # INTERNAL_API_KEY
    
  7. First apply launch should stay in manual mode with SUBMIT_FORMS=False until each provider is verified end-to-end.

Local development

cp .env.example .env
# fill in AUTH_PASSWORD_HASH, SESSION_SECRET, INTERNAL_API_KEY, creds
docker compose up -d --build
# open http://localhost:8000  (set COOKIE_SECURE=false for plain http!)

To also expose the web port locally, add ports: ["8000:8000"] under web: in docker-compose.yml (the Coolify production compose doesn't publish host ports).