Combined flat-alert + flat-apply with authenticated web UI
Find a file
EiSiMo e7f5cb9bee fix(lightbox): keep arrow clicks from leaking to backdrop close, raise controls above image
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>
2026-04-23 13:17:04 +02:00
alert refactor: rename wohnungsdidi → lazyflat 2026-04-23 09:26:12 +02:00
apply refactor: rename wohnungsdidi → lazyflat 2026-04-23 09:26:12 +02:00
web fix(lightbox): keep arrow clicks from leaking to backdrop close, raise controls above image 2026-04-23 13:17:04 +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 fix(web): read SOURCE_COMMIT directly, don't reference it in compose 2026-04-23 11:16:19 +02:00
README.md refactor: rename wohnungsdidi → lazyflat 2026-04-23 09:26:12 +02:00

lazyflat

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