Map: replace Leaflet's default marker with a divIcon SVG pin coloured
per state — green when the user has already successfully applied
(status.chip === "ok"), brand-blue otherwise. Same condition also hides
the action buttons in the popup, matching the list view, which already
hid both Bewerben and Ablehnen on success — so the only remaining
action on an applied flat is opening the original ad link.
Image gallery: clicks now open a global lightbox modal instead of a new
tab. The viewer fits each image into the viewport via max-width/height
+ object-fit: contain (uniform sizing regardless of source aspect),
shows × top-right, prev/next arrows on the sides, ←/→/Esc keyboard
nav, and click-on-backdrop to close. Prev arrow is hidden on the first
image and next on the last. Tile changes from <a target="_blank"> to
<button> since the new-tab fallback is no longer wanted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Container names, FastAPI titles, email subjects, filenames, brand text,
session cookie, User-Agent, docstrings, README. Volume lazyflat_data and
/data/lazyflat.sqlite already used the new name, so on-disk data is
preserved; dropped the now-obsolete legacy-rename comments.
Side effect: SESSION_COOKIE_NAME change logs everyone out on deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
base.html shrinks from a 150-line inline stylesheet to a single <link>;
the CSS moves to web/static/app.css byte-for-byte so there's no visual
change, but the stylesheet is now cacheable independently of the HTML.
Drop hx-on::before-request="this.disabled=true" from the Bewerben /
Ablehnen buttons — it duplicates hx-disabled-elt="find button" on the
parent form, which htmx already applies per request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Bewerben button: hx-disabled-elt + hx-on::before-request flips the
text to "läuft…" and disables the button the moment the confirm is
accepted. .btn[disabled] now renders at 55% opacity with
not-allowed cursor. Existing 3s poll interval picks up the running
state for the chip beside the address.
2. Bewerbungen tab: delete the /bewerbungen/<id> forensics detail page
+ template entirely. The list now shows a plain "Report (ZIP)"
button for every row regardless of success — same download route
(/bewerbungen/<id>/report.zip), same visual style. External link to
the listing moved onto the address itself.
3. Verified retention: web/retention.py runs cleanup_retention() hourly,
which DELETEs errors + audit_log rows older than RETENTION_DAYS (14)
and nulls applications.forensics_json for older rows. No code change
needed.
4. Partner feature. Migration v8 adds partnerships(from_user_id,
to_user_id, status, created_at, accepted_at). Einstellungen →
Partner lets users:
- send a request by username
- accept / decline incoming requests
- withdraw outgoing requests
- unlink the active partnership
A user can only have one accepted partnership; accepting one wipes
stale pending rows involving either side. On the Wohnungen list, if
the partner has applied to a flat, a small primary-colored circle
with the partner's first-name initial sits on the top-right of the
Bewerben button; if they've rejected it, the badge sits on Ablehnen.
Badge is hover-tooltipped with the partner's name + verb.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Admin → Geheimnisse sub-tab lets you edit ANTHROPIC_API_KEY +
BERLIN_WOHNEN_USERNAME/PASSWORD at runtime. Migration v7 adds a
secrets(key,value,updated_at) table; startup seeds missing keys from
env (idempotent). web reads secrets DB-first (env fallback) via
llm._api_key(); alert fetches them from web /internal/secrets on each
scan, passes them into Scraper(). Rotating creds no longer needs a
redeploy.
Masked display: 6 leading + 4 trailing chars, "…" in the middle.
Blank form fields leave the stored value untouched.
2. Drop the max_morning_commute filter from UI + server + FILTER_KEYS +
filter summary (the underlying Maps.calculate_score code stays for
potential future re-enable).
3. /static/didi.webp wired as favicon via <link rel="icon"> in base.html.
4. apply.open_page wraps page.goto in try/except so a failed load still
produces a "goto.failed" step + screenshot instead of returning an
empty forensics blob. networkidle + post-submission sleep are also
made best-effort. The error ZIP export already writes screenshot+HTML
per step and final_html — with this change every apply run leaves a
reconstructable trail even when the listing is already offline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
brand-dot is back to the blue→yellow gradient rounded square, and the
didi image sits centered inside it (span wrapper, flex-centered, 88%
inner size). The dog's transparent background lets the gradient peek
through around him.
Footer: "Programmiert für Annika ♥" → "Mit ♥ programmiert für Annika".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The image of Didi already has its own white halo, so the round crop +
extra border made it look awkward. Back to the original rounded-square
(border-radius 10px) shape and object-fit: contain so the whole dog stays
visible instead of being cropped by the circular clip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- Each row's info line now uses the enrichment JSON (rooms, size_sqm,
rent_total/rent_cold, wbs_required/wbs_type). When enrichment is still
running we show "Infos werden abgerufen…", on failure "Fehler beim
Abrufen der Infos"; the scraper fields are no longer rendered in the list
- Move the admin backfill button to the header row as a compact
"Anreichern (N)" that only appears when there's pending/failed work,
so it's findable right next to "X gefunden"
- Countdown wobble: new .countdown class forces tabular-nums + 4.2em
min-width, so neighbours stop shifting every second
- Dot spacing: pull "·" out into its own .sep span so flex gap applies
on both sides symmetrically
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply service
- POST /internal/fetch-listing: headless Playwright fetch of a listing URL,
returns {html, image_urls[], final_url}. Uses the same browser
fingerprint/profile as the apply run so bot guards don't kick in
web service
- New enrichment pipeline (web/enrichment.py):
/internal/flats → upsert → kick() enrichment in a background thread
1. POST /internal/fetch-listing on apply
2. llm.extract_flat_details(html, url) — Haiku tool-use call returns
structured JSON (address, rooms, rent, description, pros/cons, etc.)
3. Download each image directly to /data/flats/<slug>/NN.<ext>
4. Persist enrichment_json + image_count + enrichment_status on the flat
- llm.py: minimal Anthropic /v1/messages wrapper, no SDK
- DB migration v5 adds enrichment_json/_status/_updated_at + image_count
- Admin "Altbestand anreichern" button (POST /actions/enrich-all) queues
backfill for all pending/failed rows; runs in a detached task
- GET /partials/wohnung/<id> renders _wohnung_detail.html
- GET /flat-images/<slug>/<n> serves the downloaded image
UI
- Chevron on each list row toggles an inline detail pane (HTMX fetch on
first open, hx-preserve keeps it open across the 3–30 s polls)
- CSS .flat-gallery normalises image tiles to a 4/3 aspect with object-fit:
cover so different source sizes align cleanly
- "analysiert…" / "?" chips on the list reflect enrichment_status
Config
- ANTHROPIC_API_KEY + ANTHROPIC_MODEL wired into docker-compose's web
service (default model: claude-haiku-4-5-20251001)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- map_points payload now carries flat id, per-user status, can_apply, is_running
- Popup titles link to the listing; status chip mirrors the list (beworben /
läuft… / fehlgeschlagen); Bewerben + Ablehnen submit via the same HTMX
endpoints as the list, re-swapping #wohnungen-body
- csrf token rides on the script[data-csrf] sibling of #flats-map
- popupopen → htmx.process(popupEl) so hx-* on freshly injected DOM binds
- site-style .map-popup-* CSS hooked into Leaflet's popup wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* flats: new lat/lng columns (migration v3); alert geocodes every new flat
through googlemaps and ships coords in the payload
* web: CSP extended for unpkg (leaflet.css) + tile.openstreetmap.org
* Wohnungen tab: Liste/Karte view toggle (segmented, CSS-only via :has(),
selection persisted in localStorage). Karte shows passende flats as Pins
on an OSM tile map; Popup per Pin mit Adresse, Zimmer/m²/€ und Link
* Top-strip toggles are now proper iOS-style toggle switches (single
rounded knob sliding in a pill, red when on), no descriptive subtitle
* Alarm-Karte verlinkt jetzt auf /einstellungen/benachrichtigungen
(Filter-Karte bleibt /einstellungen/filter)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Einstellungen → Profil: WBS-Typ jetzt <select> mit WBS 100/140/160/180/220
* Einstellungen → Filter: Zimmer min/max als number-Feld mit step=0.5
(2.5-Zimmer-Wohnungen sauber eingebbar)
* Wohnungen-Top-Leiste: Segmented-Toggle (ein zusammenhängender Kippschalter)
für die beiden Schalter, keine einzelnen Radio-Pills mehr
* Trockenmodus umbenannt in 'Final absenden' (positive Polarität: An=echt
senden). Bestätigungsdialog beim Einschalten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* apply: Recorder.step_snap(page, name) captures both a JPEG screenshot and
the page HTML for every major moment; every provider now calls step_snap at
each logical step so failure reports contain the exact DOM and rendered
state at every stage of the flow
* ZIP report: each snapshot becomes snapshots/NN_<label>.jpg +
snapshots/NN_<label>.html for AI-assisted debugging
* web: Wohnungsliste zeigt nur noch Flats, die die eigenen Filter treffen;
Match-Chip entfernt (Liste ist jetzt implizit matchend)
* UI komplett auf Deutsch: Protokoll statt Logs, Administrator statt admin,
Trockenmodus statt dry-run, Automatik pausiert statt circuit open,
Alarm statt Alert, Abmelden statt Logout
* Wohnungen-Header: Zeile 1 Info (Alarm + Filter), Zeile 2 Schalter mit
echten Radio-Paaren (An/Aus) für Automatisch bewerben und Trockenmodus;
hx-confirm auf den kritischen Radios; per-form CSS für sichtbaren Check-State
* Protokoll: von/bis-Datumsfilter (Berliner Zeit) + CSV-Download
(/logs/export.csv) mit UTC + lokaler Zeit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* remove the kill-switch: auto-apply toggle is the single on/off; manual
'Bewerben' button now only gated by apply reachability; circuit breaker
stays but only gates auto-apply (manual bypasses, so a user can retry)
* Berlin-timezone date filter (de_dt) formats timestamps as DD.MM.YYYY HH:MM
everywhere; storage stays UTC
* Wohnungen: live 'entdeckt vor X' on every flat + 'nächste Aktualisierung in Xs'
countdown in the header, driven by /static/app.js; HTMX polls body every 30s
* drop the Fehler tab entirely; failed applications now carry a
'Fehler-Report herunterladen (ZIP)' link -> /bewerbungen/{id}/report.zip
bundles application.json, flat.json, profile_snapshot.json, forensics.json,
step_log.txt, page.html, console/errors/network JSONs, and decoded
screenshots/*.jpg for AI-assisted debugging
* trim the 'sensibel' blurb from the Profil tab
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* DB: users + user_profiles/filters/notifications/preferences; applications gets
user_id + forensics_json + profile_snapshot_json; new errors table
with 14d retention; schema versioning via MIGRATIONS list
* auth: password hashes in DB (argon2); env vars seed first admin; per-user
sessions; CSRF bound to user id
* apply: personal info/WBS moved out of env into the request body; providers
take an ApplyContext with Profile + submit_forms; full Playwright recorder
(step log, console, page errors, network, screenshots, final HTML)
* web: five top-level tabs (Wohnungen/Bewerbungen/Logs/Fehler/Einstellungen);
settings sub-tabs profil/filter/benachrichtigungen/account/benutzer;
per-user matching, auto-apply and notifications (UI/Telegram/SMTP); red
auto-apply switch on Wohnungen tab; forensics detail view for bewerbungen
and fehler; retention background thread
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the dark theme with a light/airy look: sky-blue gradient bg,
white cards with soft blue borders, sunny gradient brand mark, pastel
status chips, and readable slate tones.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three isolated services (alert scraper, apply HTTP worker, web UI+DB)
with argon2 auth, signed cookies, CSRF, rate-limited login, kill switch,
apply circuit breaker, audit log, and strict CSP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>