- App is now called "wohnungsdidi" everywhere user-facing (page title,
nav brand, login header, notification subjects, report filename,
FastAPI titles, log messages)
- Brand dot replaced with an image of Didi (web/static/didi.webp),
rendered as a round 2.25rem avatar in _layout + login
- "Programmiert für Annika ♥" footer now shows for every logged-in user,
not only Annika
- Count-up shows only seconds ("vor 73 s") regardless of age — no
rollover to minutes/hours
- Data continuity: DB file stays /data/lazyflat.sqlite and the Docker
volume stays lazyflat_data so the rename doesn't strand existing data
- Session cookie renamed to wohnungsdidi_session (one-time logout on
rollout)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
4.5 KiB
Markdown
87 lines
4.5 KiB
Markdown
# 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 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 resource** → *Docker 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**:
|
||
```bash
|
||
python -c "from argon2 import PasswordHasher; print(PasswordHasher().hash('<your-password>'))"
|
||
```
|
||
6. **Generate secrets**:
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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).
|