Commit graph

15 commits

Author SHA1 Message Date
617c76cb54 db: thread-local SQLite connections + busy_timeout
The module-level _conn was being hit from FastAPI handlers, the retention
daemon thread, and asyncio.to_thread workers simultaneously. Sharing a
single sqlite3.Connection across threads is unsafe (cursors collide)
even with check_same_thread=False and WAL. The writer _lock didn't cover
readers, so a reader cursor could race a writer mid-statement.

Switch to threading.local(): each thread gets its own Connection via
_get_conn(). WAL handles concurrent readers/writer at the DB level;
busy_timeout=5000 absorbs short-lived "database is locked" when two
threads both try to BEGIN IMMEDIATE. The write-serialising _lock stays
— it keeps multi-statement writer blocks atomic and avoids busy-loop on
concurrent writers.

External access via db._conn replaced with a new db.has_running_application
helper (the only caller outside db.py).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:01:27 +02:00
a212dff4d9 bewerben UX: instant feedback; drop forensics detail; partner feature
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>
2026-04-21 18:18:24 +02:00
3bb04210c4 secrets tab, drop commute filter, favicon, robust error reports
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>
2026-04-21 17:56:57 +02:00
0c18f0870a rename to wohnungsdidi + didi logo + footer for all + seconds-only counter
- 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>
2026-04-21 17:29:24 +02:00
da180bd7c7 ui batch: admin tab, time filter, count-up, chevron sync, tidy
1. New /admin route with sub-tabs (Protokoll, Benutzer) for admins.
   Top nav: "Protokoll" dropped, "Admin" added right of Einstellungen.
   /logs and /einstellungen/benutzer issue 301 redirects to the new paths.
   Benutzer is no longer part of Einstellungen sub-nav.
2. User_filters.max_age_hours (migration v6) — new dropdown (1–10 h /
   beliebig) under Einstellungen → Filter; Wohnungen list drops flats
   older than the cutoff by discovered_at.
3. Header shows "aktualisiert vor X s" instead of a countdown. Template
   emits data-counter-up-utc with last_alert_heartbeat; app.js ticks up
   each second. When a scrape runs, the heartbeat updates and the HTMX
   swap resets the counter naturally.
4. Chevron state synced after HTMX swaps: panes preserved via hx-preserve
   keep the user's open/closed state, and the sibling button's .open
   class is re-applied by syncFlatExpandState() on afterSwap — previously
   a scroll-triggered poll would flip the chevron back to closed while
   the pane stayed open.
5. "Final absenden" footer removed from the profile page (functionality
   is unchanged, the switch still sits atop Wohnungen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:11:58 +02:00
e0ac869425 wohnungen: drive list info from LLM JSON, tidy header
- 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>
2026-04-21 14:57:11 +02:00
eb66284172 enrichment: Haiku flat details + image gallery on expand
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>
2026-04-21 14:46:12 +02:00
de3ce19393 settings: relabel dry-run, harder PM block, rework users page
- Bewerbungen chip "Trockenmodus" → "nicht abgeschickt" (list + detail view)
- Profile form: add an off-screen honeypot (username + password) so Chrome's
  autofill burns its fill on those instead of the real E-Mail field; switch
  the visible E-Mail and Immomio-Email to type=text + inputmode=email so the
  browser heuristic no longer tags them as login emails
- Users page: create-form sits on top in its own card (3-column grid with
  Administrator checkbox inline); full-width list below with Administrator
  chip, aktiv/deaktiviert chip, "du" marker for the current user, plus
  disable/activate and a new red "löschen" button (confirm prompt) wired to
  new POST /actions/users/delete which cascades through the user's data

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:11:50 +02:00
7f7cbb5b1f cleanup: drop coord backfill, drop transit overlay, block PM autofill
- Remove the admin "Koordinaten nachladen" button, /actions/backfill-coords
  endpoint, geocode.py, googlemaps dep, GMAPS_API_KEY plumbing in the web
  service, and the map diagnostic line. Going-forward geocoding happens in
  alert on scrape; upsert_flat backfill on re-submit remains for edge cases
- Remove the OpenRailwayMap transit overlay (visually noisy); keep CartoDB
  Voyager as the sole basemap
- Profile + notifications forms get autocomplete="off" + data-lpignore +
  data-1p-ignore at form and field level to keep password managers from
  popping open on /einstellungen; immomio_password uses autocomplete=new-password

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:55:24 +02:00
0c58242ce7 map debug + coord backfill, remove email channel, countdown label
- Surface "X/Y passende Wohnungen mit Koordinaten" on the Karte view +
  admin-only "Koordinaten nachladen" button (POST /actions/backfill-coords)
  that geocodes missing flats via Google Maps directly from the web container
- Add googlemaps dep + GMAPS_API_KEY env to web service
- Light console.log in map.js ("rendering N/M markers", "building Leaflet…")
  so the browser DevTools shows what's happening
- Drop e-mail channel from notifications UI, notify dispatcher, and _alert_status;
  coerce legacy 'email' channel rows back to 'ui' on save
- Countdown said "Aktualisierung läuft…" next to "nächste Aktualisierung" →
  shortened to "aktualisiere…"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:42:21 +02:00
51b6b02b24 wohnungen: preserve map across HTMX polls, add rejected section, drop €/m²
- #flats-map uses hx-preserve; marker data moved to <script type="application/json">
  sibling that diffs+updates instead of rebuilding Leaflet every poll (fixes
  whitescreen + tiles rendering outside the card)
- upsert_flat backfills lat/lng on existing rows missing coords (older flats
  scraped before the lat/lng migration now appear on the map once alert re-submits)
- collapsible "Abgelehnte Wohnungen" section at the bottom with Wiederherstellen
- remove €/m² column from the list

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:33:55 +02:00
Moritz
42377f0b67 UX: alarm-status, ablehnen-button, annika-footer, map polish
* Alarm-Status ist jetzt nur 'aktiv' wenn ein echter Push-Channel (Telegram
  mit Token+Chat oder E-Mail mit Adresse) konfiguriert ist. UI-only zählt
  nicht mehr als eingerichteter Alarm.
* Ablehnen-Button in der Wohnungsliste: flat_rejections (migration v4)
  speichert pro-User-Ablehnungen, abgelehnte Flats fallen aus Liste und
  Karte raus. Wiederholbar pro User unabhängig.
* Footer 'Programmiert für Annika ♥' erscheint nur auf Seiten, wenn annika
  angemeldet ist.
* Map: Hinweistext unter leerer Karte entfernt; alle Zoom-Mechanismen
  deaktiviert (Scrollrad, Doppelklick, Box, Touch, Tastatur, +/- Buttons).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:09:44 +02:00
Moritz
376551213a map view (Leaflet + OSM), iOS switches, Alarm → Benachrichtigungen
* 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>
2026-04-21 12:02:40 +02:00
Moritz
c630b500ef multi-user: users, per-user profiles/filters/notifications, tab UI, apply forensics
* 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>
2026-04-21 10:52:41 +02:00
Moritz
69f2f1f635 lazyflat: combined alert + apply behind authenticated web UI
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>
2026-04-21 09:51:35 +02:00