Combined flat-alert + flat-apply with authenticated web UI
Logs from the user's last session showed that after walking through all images and trying to go back from the last one, a backdrop click fired (target===overlay) and closed the modal — even though the user believed they clicked the prev arrow. Two reinforcing causes: 1. The image (.lightbox-image) is a sibling AFTER the buttons in the DOM with no z-index, so paint order put the image on top of the absolute-positioned arrows. Where the image's max-width/height box overlapped the arrows, clicks landed on the image instead of the arrow, and clicks in the gap between image and arrow hit the overlay backdrop. 2. Even when an arrow handler did fire, the click bubbled up to the overlay's click handler. While target===overlay was false in that path, the next click sometimes did land on the backdrop, and the close button had the same exposure. Fix: - Stack the controls above the image: image gets z-index:1, every .lightbox button gets z-index:2. - stopPropagation on prev/next/close button clicks AND on the image click — guarantees they can never bubble into the overlay's backdrop-close handler. Backdrop close still works on actual backdrop clicks. - Bump button background to rgba(0,0,0,.55) (was .08 white on dark) so the arrows are clearly visible against the image. Also strip the [lazyflat.lightbox] DEBUG(lightbox) tracer logs and the window.error catch-all — original symptom is fixed and the existing flow is confirmed working in user's logs. 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).