lazyflat/web/templates/_wohnungen_body.html
Moritz 7444f90d6a per-step screenshot + html snapshots, matches-only list, full German UI, CSV export
* 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>
2026-04-21 11:40:12 +02:00

160 lines
7.7 KiB
HTML

{# Renders the full Wohnungen body; used both by the full page and by HTMX poll. #}
<div id="wohnungen-body"
class="space-y-5"
hx-get="/partials/wohnungen"
hx-trigger="every {{ poll_interval }}s"
hx-swap="outerHTML">
<!-- 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">
<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>
</div>
</a>
<a class="card px-4 py-2.5 flex flex-col gap-0.5 hover:bg-[#f6fafd]" href="/einstellungen/filter">
<div class="text-[11px] uppercase tracking-wide text-slate-500">Filter</div>
<div class="text-sm text-slate-700 truncate">{{ filter_summary }}</div>
</a>
</section>
<!-- Reihe 2: Schalter Automatisch bewerben + Trockenmodus (Radio-Gruppen) -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Automatisch bewerben -->
<form class="card p-4"
hx-post="/actions/auto-apply"
hx-trigger="change"
hx-target="#wohnungen-body"
hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}">
<div class="text-[11px] uppercase tracking-wide text-slate-500 mb-2">Automatisch bewerben</div>
<div class="flex gap-4">
<label class="radio-opt">
<input type="radio" name="value" value="off"
{% if not auto_apply_enabled %}checked{% endif %}>
<span>Aus</span>
</label>
<label class="radio-opt"
{% if not auto_apply_enabled %}data-hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben."{% endif %}>
<input type="radio" name="value" value="on"
hx-confirm="Automatisches Bewerben einschalten? Bei jedem passenden Flat wird automatisch beworben."
{% if auto_apply_enabled %}checked{% endif %}>
<span>An</span>
</label>
</div>
</form>
<!-- Trockenmodus -->
<form class="card p-4"
hx-post="/actions/submit-forms"
hx-trigger="change"
hx-target="#wohnungen-body"
hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}">
<div class="text-[11px] uppercase tracking-wide text-slate-500 mb-2">Trockenmodus</div>
<div class="flex gap-4">
<label class="radio-opt">
<input type="radio" name="value" value="on"
{% if not submit_forms %}checked{% endif %}>
<span>An <span class="text-xs text-slate-500">(Formular ausfüllen, nicht absenden)</span></span>
</label>
<label class="radio-opt">
<input type="radio" name="value" value="off"
hx-confirm="Trockenmodus ausschalten? Formulare werden dann WIRKLICH abgesendet!"
{% if submit_forms %}checked{% endif %}>
<span>Aus <span class="text-xs text-[#b8404e]">(echt senden)</span></span>
</label>
</div>
</form>
</section>
{% if not apply_allowed %}
<div class="card p-3 text-sm">
<span class="chip chip-bad">Bewerbungs-Dienst nicht erreichbar</span>
<span class="ml-2 text-slate-600">{{ apply_block_reason }}</span>
</div>
{% endif %}
{% if circuit_open %}
<div class="card p-3 text-sm flex items-center justify-between">
<div>
<span class="chip chip-bad">Automatik pausiert</span>
<span class="ml-2 text-slate-600">{{ apply_failures }} Fehler in Folge</span>
</div>
<form method="post" action="/actions/reset-circuit"
hx-post="/actions/reset-circuit" hx-target="#wohnungen-body" hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}">
<button class="btn btn-ghost text-xs" type="submit">Zurücksetzen</button>
</form>
</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 %}
</div>
</div>
<div class="divide-y divide-soft">
{% for item in flats %}
{% set f = item.row %}
<div class="px-4 py-3 flex flex-col md:flex-row md:items-center gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<a class="font-medium truncate" href="{{ f.link }}" target="_blank" rel="noopener noreferrer">
{{ f.address or f.link }}
</a>
{% if item.last and item.last.finished_at is none %}
<span class="chip chip-warn">läuft…</span>
{% elif item.last and item.last.success == 1 %}<span class="chip chip-ok">beworben</span>
{% elif item.last and item.last.success == 0 %}<span class="chip chip-bad">fehlgeschlagen</span>
{% endif %}
</div>
<div class="text-xs text-slate-500 mt-0.5">
{% if f.rooms %}{{ "%.1f"|format(f.rooms) }} Z{% endif %}
{% if f.size %} · {{ "%.0f"|format(f.size) }} m²{% endif %}
{% if f.total_rent %} · {{ "%.0f"|format(f.total_rent) }} €{% endif %}
{% if f.sqm_price %} ({{ "%.2f"|format(f.sqm_price) }} €/m²){% endif %}
{% if f.connectivity_morning_time %} · {{ "%.0f"|format(f.connectivity_morning_time) }} min morgens{% endif %}
{% if f.wbs %} · WBS: {{ f.wbs }}{% endif %}
· entdeckt <span data-rel-utc="{{ f.discovered_at|iso_utc }}" title="{{ f.discovered_at|de_dt }}"></span>
</div>
{% if item.last and item.last.message %}
<div class="text-xs text-slate-500 mt-1 truncate">↳ {{ item.last.message }}</div>
{% endif %}
</div>
<div class="flex gap-2">
{% if apply_allowed and not (item.last and item.last.success == 1) %}
{% set is_running = item.last and item.last.finished_at is none %}
<form method="post" action="/actions/apply"
hx-post="/actions/apply" hx-target="#wohnungen-body" hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}">
<input type="hidden" name="flat_id" value="{{ f.id }}">
<button class="btn btn-primary text-sm" type="submit"
{% if is_running %}disabled{% endif %}
hx-confirm="Bewerbung für {{ (f.address or f.link)|e }} starten?">
{% if is_running %}läuft…{% else %}Bewerben{% endif %}
</button>
</form>
{% endif %}
</div>
</div>
{% else %}
<div class="px-4 py-8 text-center text-slate-500">
{% if alert_label == 'nicht eingerichtet' %}
Bitte zuerst Filter einstellen, damit passende Wohnungen angezeigt werden.
{% else %}
Aktuell keine Wohnung, die alle Filter erfüllt.
{% endif %}
</div>
{% endfor %}
</div>
</section>
</div>