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>
This commit is contained in:
Moritz 2026-04-21 12:02:40 +02:00
parent d9468f6814
commit 376551213a
8 changed files with 239 additions and 83 deletions

View file

@ -7,7 +7,7 @@
<!-- Reihe 1: Info-Kacheln Alarm + Filter -->
<section class="grid grid-cols-2 gap-3">
<a class="card px-4 py-2.5 flex flex-col gap-0.5 hover:bg-[#f6fafd]" href="/einstellungen/filter">
<a class="card px-4 py-2.5 flex flex-col gap-0.5 hover:bg-[#f6fafd]" href="/einstellungen/benachrichtigungen">
<div class="text-[11px] uppercase tracking-wide text-slate-500">Alarm</div>
<div class="flex items-center gap-2">
<span class="chip chip-{{ alert_chip }}">{{ alert_label }}</span>
@ -21,68 +21,36 @@
<!-- Reihe 2: Schalter Automatisch bewerben + Final absenden -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Automatisch bewerben -->
<form class="card p-4 flex items-center justify-between gap-3">
<input type="hidden" name="csrf" value="{{ csrf }}">
<div class="flex flex-col gap-0.5">
<div class="text-[11px] uppercase tracking-wide text-slate-500">Automatisch bewerben</div>
<div class="text-xs text-slate-500">bei Match ohne Nachfrage bewerben</div>
</div>
<div class="toggle warn">
<label>
<input type="radio" name="value" value="off"
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
{% if not auto_apply_enabled %}checked{% endif %}>
Aus
</label>
<label>
<input type="radio" name="value" value="on"
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben."
{% if auto_apply_enabled %}checked{% endif %}>
An
</label>
</div>
<div class="text-sm font-medium">Automatisch bewerben</div>
<label class="switch warn">
<input type="checkbox" name="value" value="on"
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
{% if not auto_apply_enabled %}hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben."{% endif %}
{% if auto_apply_enabled %}checked{% endif %}>
<span class="switch-visual"></span>
</label>
</form>
<!-- Final absenden (inverse of submit_forms: on=real, off=trocken) -->
<form class="card p-4 flex items-center justify-between gap-3">
<input type="hidden" name="csrf" value="{{ csrf }}">
<div class="flex flex-col gap-0.5">
<div class="text-[11px] uppercase tracking-wide text-slate-500">Final absenden</div>
<div class="text-xs text-slate-500">aus = Formular ausfüllen, nicht abschicken</div>
</div>
<div class="toggle warn">
<label>
<input type="radio" name="value" value="off"
hx-post="/actions/submit-forms"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
{% if not submit_forms %}checked{% endif %}>
Aus
</label>
<label>
<input type="radio" name="value" value="on"
hx-post="/actions/submit-forms"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
hx-confirm="Final absenden einschalten? Formulare werden dann WIRKLICH abgeschickt!"
{% if submit_forms %}checked{% endif %}>
An
</label>
</div>
<div class="text-sm font-medium">Final absenden</div>
<label class="switch warn">
<input type="checkbox" name="value" value="on"
hx-post="/actions/submit-forms"
hx-trigger="change"
hx-include="closest form"
hx-target="#wohnungen-body"
hx-swap="outerHTML"
{% if not submit_forms %}hx-confirm="Final absenden einschalten? Formulare werden dann WIRKLICH abgeschickt!"{% endif %}
{% if submit_forms %}checked{% endif %}>
<span class="switch-visual"></span>
</label>
</form>
</section>
@ -107,17 +75,42 @@
</div>
{% endif %}
<!-- Liste passender Wohnungen -->
<section class="card">
<div class="flex items-center justify-between px-4 py-3 border-b border-soft gap-4 flex-wrap">
<h2 class="font-semibold">Passende Wohnungen auf inberlinwohnen.de</h2>
<div class="text-xs text-slate-500 flex gap-3 items-center">
<span>{{ flats|length }} gefunden</span>
{% if next_scrape_utc %}
<span>· nächste Aktualisierung <span data-countdown-utc="{{ next_scrape_utc }}"></span></span>
{% endif %}
<!-- Header + View-Toggle (Liste/Karte) -->
<section class="flex items-center justify-between gap-4 flex-wrap">
<h2 class="font-semibold">Passende Wohnungen auf inberlinwohnen.de</h2>
<div class="flex items-center gap-3 text-xs text-slate-500">
<span>{{ flats|length }} gefunden</span>
{% if next_scrape_utc %}
<span>· nächste Aktualisierung <span data-countdown-utc="{{ next_scrape_utc }}"></span></span>
{% endif %}
<div class="view-toggle ml-2">
<label>
<input type="radio" name="view_mode" id="v_list" value="list" checked>
Liste
</label>
<label>
<input type="radio" name="view_mode" id="v_map" value="map">
Karte
</label>
</div>
</div>
</section>
<!-- Karte -->
<section class="view-map">
<div class="card p-3">
<div id="flats-map" data-flats='{{ map_points | tojson }}'></div>
{% if not map_points %}
<p class="mt-3 text-xs text-slate-500">
Keine Koordinaten für passende Wohnungen vorhanden —
entweder sind noch keine neuen Flats geocoded worden oder die Filter lassen noch nichts durch.
</p>
{% endif %}
</div>
</section>
<!-- Liste -->
<section class="view-list card">
<div class="divide-y divide-soft">
{% for item in flats %}
{% set f = item.row %}