Combined flat-alert + flat-apply with authenticated web UI
Per review §1 — verified no callers before each deletion:
- _next_scrape_utc (context dict key never read by any template)
- ALERT_SCRAPE_INTERVAL_SECONDS settings import (only _next_scrape_utc read it)
- alert/paths.py (imported by nothing)
- alert/settings.py LANGUAGE (alert doesn't use translations.toml)
- alert/main.py: the vestigial `c = {}` connectivity dict, the comment
about re-enabling it, and the entire connectivity block in
_flat_payload — the web-side columns stay NULL on insert now
- alert/maps.py: DESTINATIONS, calculate_score, _get_next_weekday,
_calculate_transfers (only geocode is used in the scraper)
- alert/flat.py: connectivity + display_address properties,
_connectivity field, unused datetime import
- apply/utils.py str_to_preview (no callers) — file removed
- web/matching.py: max_morning_commute + commute check
- web/app.py: don't pass connectivity dict into flat_matches_filter,
don't write email_address through update_notifications
- web/db.py: get_error (no callers); drop kill_switch,
max_morning_commute, email_address from their allowed-sets so they're
not writable through update_* anymore
- web/settings.py + docker-compose.yml: SMTP_HOST/PORT/USERNAME/PASSWORD/
FROM/STARTTLS (notifications.py is telegram-only now)
DB columns themselves (kill_switch, email_address, max_morning_commute,
connectivity_morning_time, connectivity_night_time) stay in the schema
— SQLite can't drop them cheaply and they're harmless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| alert | ||
| apply | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
wohnungsdidi
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/wohnungsdidi.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).