Combined flat-alert + flat-apply with authenticated web UI
* Wohnungen header: single slim row with Alert status · Filter summary · Auto-Bewerben toggle · Trockenmodus toggle. Big filter panel removed — filters live only in /einstellungen/filter. * Alert status: 'nicht eingerichtet' until the user has actual filters (+ valid notification creds if telegram/email). 'aktiv' otherwise. * Logs tab: admin-only (gated both in layout and server-side). Shows merged audit + errors across all users, sorted newest-first, capped at 300. * Apply, auto-apply, trockenmodus and circuit reset buttons post via HTMX and swap the Wohnungen body. While any application is still running for the user the poll interval drops from 30s to 3s so status flips to 'beworben' or 'fehlgeschlagen' almost immediately. * Browser tab title is now always 'lazyflat'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| alert | ||
| apply | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
lazyflat
Combined deployment of flat-alert (reliable scraper) and flat-apply (experimental auto‑applier) 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 toweb. Zero external dependencies onapply, soapplycrashes can never bring the alerter down.apply/— FastAPI wrapper around the experimental Playwright applier. Only accepts requests with a sharedX-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 sharedINTERNAL_API_KEYand never exposed by Coolify.
Deployment on Coolify
- Create repo: push this monorepo to
ssh://git@git.moritz.run:2222/moritz/lazyflat.git. - New Coolify resource → Docker Compose → point it at this repo. Coolify
will read
docker-compose.ymland deploy all three services on one network. - Domain: set
flat.lab.moritz.runon thewebservice only. Coolify (Traefik) handles TLS. Do not set a domain onalertorapply. - Secrets: paste the environment variables from
.env.exampleinto Coolify's env UI. At minimum you need:AUTH_USERNAME,AUTH_PASSWORD_HASHSESSION_SECRET,INTERNAL_API_KEYGMAPS_API_KEY,BERLIN_WOHNEN_USERNAME,BERLIN_WOHNEN_PASSWORD- personal info + WBS for
apply
- Generate the password hash:
python -c "from argon2 import PasswordHasher; print(PasswordHasher().hash('<your-password>'))" - 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 - First apply launch should stay in manual mode with
SUBMIT_FORMS=Falseuntil 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).