Combined flat-alert + flat-apply with authenticated web UI
- notifications: round sqm_price to whole € in Telegram match messages
(was emitting raw float like "12.345614 €/m²").
- wohnungen: remove the admin-only "Bilder nachladen (N)" button. It
flickered into view whenever a freshly-scraped flat was still in
pending state, which was effectively random from the user's point of
view, and the manual backfill it triggered isn't needed anymore — new
flats are auto-enriched at scrape time. Also drops the dead helpers
it was the sole caller of: enrichment.kick_backfill,
enrichment._backfill_runner, db.flats_needing_enrichment,
db.enrichment_counts.
- lightbox: the modal didn't appear because Tailwind's Play CDN injects
its own .hidden { display: none } rule at runtime, which kept fighting
our class toggle. Switch the show/hide to inline style.display so no
external stylesheet can mask it. Single-class .lightbox now only owns
the layout — the initial-hidden state is on the element via
style="display:none".
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).