Combined flat-alert + flat-apply with authenticated web UI
Find a file
EiSiMo f1e26b38d0 refactor: split web/app.py into routers
app.py was ~1300 lines with every route, helper, and middleware mixed
together. Split into:

- app.py (~100 lines): FastAPI bootstrap, lifespan, /health, security
  headers, Jinja filter registration, include_router calls
- common.py: shared helpers (templates, apply_client, base_context,
  _is_htmx, client_ip, require_internal, time helpers, filter helpers,
  apply-gate helpers, _kick_apply / _finish_apply_background,
  _bg_tasks, _spawn, _mask_secret, _has_running_application, BERLIN_TZ)
- routes/auth.py: /login (GET+POST), /logout
- routes/wohnungen.py: /, /partials/wohnungen, /partials/wohnung/{id},
  /flat-images/{slug}/{idx}, /actions/apply|reject|unreject|auto-apply|
  submit-forms|reset-circuit|filters|enrich-all|enrich-flat; owns
  _wohnungen_context + _wohnungen_partial_or_redirect
- routes/bewerbungen.py: /bewerbungen, /bewerbungen/{id}/report.zip
- routes/einstellungen.py: /einstellungen, /einstellungen/{section},
  /actions/profile|notifications|account/password|partner/*; owns
  VALID_SECTIONS
- routes/admin.py: /logs redirect, /admin, /admin/{section},
  /logs/export.csv, /actions/users/*|secrets; owns ADMIN_SECTIONS,
  _parse_date_range, _collect_events
- routes/internal.py: /internal/flats|heartbeat|error|secrets

Route-diff before/after is empty — all 41 routes + /static mount
preserved. No behavior changes, pure mechanical split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:27:12 +02:00
alert correctness batch: atomic writes, task refs, hmac, import-star, pickle 2026-04-21 19:14:26 +02:00
apply correctness batch: atomic writes, task refs, hmac, import-star, pickle 2026-04-21 19:14:26 +02:00
web refactor: split web/app.py into routers 2026-04-21 19:27: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 chore: sweep dead code across all three services 2026-04-21 19:06:05 +02:00
README.md rename to wohnungsdidi + didi logo + footer for all + seconds-only counter 2026-04-21 17:29:24 +02:00

wohnungsdidi

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/wohnungsdidi.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).