ui: live timers, Berlin timestamps, ZIP failure reports, drop kill-switch/Fehler tab

* 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>
This commit is contained in:
Moritz 2026-04-21 11:09:37 +02:00
parent c630b500ef
commit 332d9eea19
13 changed files with 429 additions and 343 deletions

View file

@ -21,7 +21,6 @@
<a class="tab {% if active_tab=='wohnungen' %}active{% endif %}" href="/">Wohnungen</a>
<a class="tab {% if active_tab=='bewerbungen' %}active{% endif %}" href="/bewerbungen">Bewerbungen</a>
<a class="tab {% if active_tab=='logs' %}active{% endif %}" href="/logs">Logs</a>
<a class="tab {% if active_tab=='fehler' %}active{% endif %}" href="/fehler">Fehler</a>
<a class="tab {% if active_tab=='einstellungen' %}active{% endif %}" href="/einstellungen">Einstellungen</a>
</nav>
</header>

View file

@ -1,8 +1,4 @@
<h2 class="font-semibold mb-4">Bewerbungsdaten</h2>
<p class="text-sm text-slate-600 mb-4">
Diese Angaben werden beim Bewerben an die jeweilige Website gesendet.
<span class="chip chip-warn">sensibel</span> — werden nur in der DB gespeichert und pro Bewerbung als Snapshot protokolliert.
</p>
<form method="post" action="/actions/profile" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input type="hidden" name="csrf" value="{{ csrf }}">

View file

@ -0,0 +1,178 @@
{# Renders the full Wohnungen body; used both by the full page and by HTMX poll. #}
<div id="wohnungen-body" class="space-y-6">
<!-- Auto-Bewerben + Status-Leiste -->
<section class="card p-5 flex flex-col md:flex-row md:items-center gap-4 justify-between">
<div class="flex flex-col gap-1">
<div class="text-xs uppercase tracking-wide text-slate-500">Auto-Bewerben</div>
<div class="flex items-center gap-2">
{% if auto_apply_enabled %}
<span class="chip chip-warn">aktiviert</span>
<span class="text-sm text-slate-600">bei Match wird automatisch beworben</span>
{% else %}
<span class="chip chip-info">deaktiviert</span>
<span class="text-sm text-slate-600">Matches werden nur angezeigt</span>
{% endif %}
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<form method="post" action="/actions/auto-apply">
<input type="hidden" name="csrf" value="{{ csrf }}">
<input type="hidden" name="value" value="{% if auto_apply_enabled %}off{% else %}on{% endif %}">
<button class="btn btn-hot {% if not auto_apply_enabled %}off{% endif %}"
onclick="return confirm('{% if auto_apply_enabled %}Auto-Bewerben deaktivieren?{% else %}Auto-Bewerben aktivieren? Das System bewirbt dann automatisch bei jedem Match bitte Profil und Filter prüfen.{% endif %}');"
type="submit">
{% if auto_apply_enabled %}AUTO-BEWERBEN DEAKTIVIEREN{% else %}AUTO-BEWERBEN AKTIVIEREN{% endif %}
</button>
</form>
{% if circuit_open %}
<form method="post" action="/actions/reset-circuit">
<input type="hidden" name="csrf" value="{{ csrf }}">
<button class="btn btn-ghost text-sm" type="submit">Circuit zurücksetzen</button>
</form>
{% endif %}
</div>
</section>
{% if not apply_allowed %}
<div class="card p-4">
<span class="chip chip-bad">apply blockiert</span>
<span class="ml-2 text-sm text-slate-600">{{ apply_block_reason }}</span>
</div>
{% endif %}
<!-- Status-Reihe -->
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-3">
<div class="text-xs text-slate-500">alert</div>
<div class="mt-1">
{% if last_alert_heartbeat %}<span class="chip chip-ok">live</span>
{% else %}<span class="chip chip-warn">kein Heartbeat</span>{% endif %}
</div>
</div>
<div class="card p-3">
<div class="text-xs text-slate-500">apply</div>
<div class="mt-1">
{% if apply_reachable %}<span class="chip chip-ok">ok</span>
{% else %}<span class="chip chip-bad">down</span>{% endif %}
</div>
</div>
<div class="card p-3">
<div class="text-xs text-slate-500">submit_forms</div>
<div class="mt-1">
{% if submit_forms %}<span class="chip chip-warn">echt senden</span>
{% else %}<span class="chip chip-info">dry-run</span>{% endif %}
</div>
</div>
<div class="card p-3">
<div class="text-xs text-slate-500">Fehler in Serie</div>
<div class="mt-1">
{% if circuit_open %}<span class="chip chip-bad">circuit open</span>
{% elif apply_failures > 0 %}<span class="chip chip-warn">{{ apply_failures }}</span>
{% else %}<span class="chip chip-ok">0</span>{% endif %}
</div>
</div>
</section>
<!-- Filter Panel -->
<details class="card" {% if not filters.rooms_min and not filters.max_rent %}open{% endif %}>
<summary class="px-5 py-3 font-semibold select-none">Eigene Filter</summary>
<form method="post" action="/actions/filters" class="p-5 grid grid-cols-2 md:grid-cols-3 gap-4">
<input type="hidden" name="csrf" value="{{ csrf }}">
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer min</label>
<input class="input" name="rooms_min" value="{{ filters.rooms_min if filters.rooms_min is not none else '' }}" placeholder="z.B. 2">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer max</label>
<input class="input" name="rooms_max" value="{{ filters.rooms_max if filters.rooms_max is not none else '' }}" placeholder="z.B. 3">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">max Miete (€)</label>
<input class="input" name="max_rent" value="{{ filters.max_rent if filters.max_rent is not none else '' }}" placeholder="1500">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">min Größe (m²)</label>
<input class="input" name="min_size" value="{{ filters.min_size if filters.min_size is not none else '' }}" placeholder="40">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">max Anfahrt morgens (min)</label>
<input class="input" name="max_morning_commute" value="{{ filters.max_morning_commute if filters.max_morning_commute is not none else '' }}" placeholder="50">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">WBS benötigt</label>
<select class="input" name="wbs_required">
<option value="" {% if not filters.wbs_required %}selected{% endif %}>egal</option>
<option value="yes" {% if filters.wbs_required == 'yes' %}selected{% endif %}>ja</option>
<option value="no" {% if filters.wbs_required == 'no' %}selected{% endif %}>nein</option>
</select>
</div>
<div class="col-span-2 md:col-span-3 flex gap-2 pt-2">
<button class="btn btn-primary" type="submit">Filter speichern</button>
<span class="text-xs text-slate-500 self-center">Leer lassen = kein Limit. Filter bestimmen Match-Hervorhebung + Auto-Bewerben.</span>
</div>
</form>
</details>
<!-- Liste aller 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">Neueste Wohnungen auf inberlinwohnen.de</h2>
<div class="text-xs text-slate-500 flex gap-3 items-center">
<span>{{ flats|length }} gesehen</span>
{% if next_scrape_utc %}
<span>· nächste Aktualisierung <span data-countdown-utc="{{ next_scrape_utc }}"></span></span>
{% else %}
<span>· warte auf alert…</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 {% if item.matched %}bg-[#f2f8ff]{% endif %}">
<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.matched %}<span class="chip chip-ok">match</span>{% endif %}
{% if 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">apply fehlgeschlagen</span>
{% elif item.last %}<span class="chip chip-warn">läuft</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) %}
<form method="post" action="/actions/apply">
<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"
onclick="return confirm('Bewerbung für {{ (f.address or f.link)|e }} ausführen?');">
Bewerben
</button>
</form>
{% endif %}
</div>
</div>
{% else %}
<div class="px-4 py-8 text-center text-slate-500">Noch keine Wohnungen entdeckt.</div>
{% endfor %}
</div>
</section>
</div>

View file

@ -7,6 +7,7 @@
<title>{% block title %}lazyflat{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<script src="/static/app.js" defer></script>
<style>
:root {
--bg-from: #e4f0fb; --bg-to: #f7fbfe;

View file

@ -13,11 +13,18 @@
{% if application.provider %}<span class="chip chip-info">{{ application.provider }}</span>{% endif %}
{% if application.submit_forms_used %}<span class="chip chip-warn">echt gesendet</span>
{% else %}<span class="chip chip-info">dry-run</span>{% endif %}
{% if application.success == 0 %}
<a class="btn btn-danger text-sm ml-auto"
href="/bewerbungen/{{ application.id }}/report.zip">
Fehler-Report herunterladen (ZIP)
</a>
{% endif %}
</div>
<div class="text-sm text-slate-600 space-y-1">
<div><span class="text-slate-500">URL:</span> <a href="{{ application.url }}" target="_blank" rel="noopener">{{ application.url }}</a></div>
<div><span class="text-slate-500">gestartet:</span> {{ application.started_at }}</div>
<div><span class="text-slate-500">beendet:</span> {{ application.finished_at or "—" }}</div>
<div><span class="text-slate-500">gestartet:</span> {{ application.started_at|de_dt }}</div>
<div><span class="text-slate-500">beendet:</span> {{ application.finished_at|de_dt if application.finished_at else "—" }}</div>
{% if application.message %}<div><span class="text-slate-500">Meldung:</span> {{ application.message }}</div>{% endif %}
</div>
</section>
@ -31,7 +38,7 @@
{% if forensics %}
<section class="card p-5 space-y-4">
<h3 class="font-semibold">Forensik (für KI-Debug)</h3>
<h3 class="font-semibold">Forensik</h3>
<details open>
<summary class="font-medium">Step-Log ({{ forensics.steps|length }} Einträge, {{ forensics.duration_s }} s)</summary>

View file

@ -11,18 +11,24 @@
<div class="px-4 py-3 text-sm">
<div class="flex items-center gap-2 flex-wrap">
{% if a.success == 1 %}<span class="chip chip-ok">ok</span>
{% elif a.success == 0 %}<span class="chip chip-bad">fail</span>
{% elif a.success == 0 %}<span class="chip chip-bad">fehlgeschlagen</span>
{% else %}<span class="chip chip-warn">läuft</span>{% endif %}
<span class="chip chip-info">{{ a.triggered_by }}</span>
{% if a.provider %}<span class="chip chip-info">{{ a.provider }}</span>{% endif %}
{% if a.submit_forms_used %}<span class="chip chip-warn">echt gesendet</span>
{% else %}<span class="chip chip-info">dry-run</span>{% endif %}
<span class="text-slate-500 text-xs ml-auto">{{ a.started_at }}</span>
<span class="text-slate-500 text-xs ml-auto"
title="{{ a.started_at|de_dt }}">{{ a.started_at|de_dt }}</span>
</div>
<div class="mt-1 truncate">
<a href="/bewerbungen/{{ a.id }}">#{{ a.id }} — {{ a.address or a.url }}</a>
</div>
{% if a.message %}<div class="text-xs text-slate-500 mt-0.5 truncate">{{ a.message }}</div>{% endif %}
{% if a.success == 0 %}
<div class="mt-1">
<a class="text-xs" href="/bewerbungen/{{ a.id }}/report.zip">↓ Fehler-Report (ZIP)</a>
</div>
{% endif %}
</div>
{% else %}
<div class="px-4 py-8 text-center text-slate-500">Noch keine Bewerbungen.</div>

View file

@ -1,29 +0,0 @@
{% extends "_layout.html" %}
{% block title %}Fehler — lazyflat{% endblock %}
{% block content %}
<section class="card">
<div class="px-4 py-3 border-b border-soft flex items-center justify-between">
<h2 class="font-semibold">Fehler {% if is_admin %}<span class="chip chip-info ml-2">inkl. globaler</span>{% endif %}</h2>
<span class="text-xs text-slate-500">{{ errors|length }}</span>
</div>
<div class="divide-y divide-soft">
{% for e in errors %}
<a class="block px-4 py-3 hover:bg-[#f6fafd]" href="/fehler/{{ e.id }}">
<div class="flex items-center gap-2 flex-wrap">
<span class="chip chip-bad">{{ e.kind }}</span>
<span class="chip chip-info">{{ e.source }}</span>
{% if e.application_id %}<span class="chip chip-info">#{{ e.application_id }}</span>{% endif %}
<span class="text-xs text-slate-500 ml-auto">{{ e.timestamp }}</span>
</div>
<div class="text-sm mt-1 truncate">{{ e.summary or "(kein Text)" }}</div>
</a>
{% else %}
<div class="px-4 py-8 text-center text-slate-500">Keine Fehler — läuft rund.</div>
{% endfor %}
</div>
<div class="px-4 py-3 border-t border-soft text-xs text-slate-500">
Fehler werden 14 Tage aufbewahrt. Bei fehlgeschlagenen Bewerbungen enthält die Detailseite Screenshots,
Step-Log, Browser-Konsole + Netzwerk-Trace für KI-gestützte Fehleranalyse.
</div>
</section>
{% endblock %}

View file

@ -1,32 +0,0 @@
{% extends "_layout.html" %}
{% block title %}Fehler #{{ error.id }} — lazyflat{% endblock %}
{% block content %}
<a href="/fehler" class="text-sm">← zurück zu den Fehlern</a>
<section class="card p-5 space-y-2">
<div class="flex items-center gap-2 flex-wrap">
<h2 class="font-semibold text-lg">Fehler #{{ error.id }}</h2>
<span class="chip chip-bad">{{ error.kind }}</span>
<span class="chip chip-info">{{ error.source }}</span>
</div>
<div class="text-sm text-slate-600">
{{ error.timestamp }} · {{ error.summary or "(kein Text)" }}
</div>
{% if context %}
<details class="mt-2">
<summary class="font-medium">Kontext</summary>
<pre class="mono whitespace-pre-wrap break-all mt-2 p-3 bg-[#f6fafd] rounded-lg border border-soft">{{ context | tojson(indent=2) }}</pre>
</details>
{% endif %}
</section>
{% if application %}
<section class="card p-5">
<h3 class="font-semibold mb-2">Zugehörige Bewerbung</h3>
<div class="text-sm">
<a href="/bewerbungen/{{ application.id }}">Bewerbung #{{ application.id }} öffnen</a>
— vollständige Forensik dort.
</div>
</section>
{% endif %}
{% endblock %}

View file

@ -6,7 +6,7 @@
<div class="divide-y divide-soft">
{% for e in events %}
<div class="px-4 py-2 mono">
<span class="text-slate-500">{{ e.timestamp }}</span>
<span class="text-slate-500">{{ e.timestamp|de_dt }}</span>
<span class="text-slate-400">{{ e.actor }}</span>
<span class="text-slate-700">{{ e.action }}</span>
{% if e.details %}<span class="text-slate-500">— {{ e.details }}</span>{% endif %}

View file

@ -1,182 +1,10 @@
{% extends "_layout.html" %}
{% block title %}Wohnungen — lazyflat{% endblock %}
{% block content %}
<!-- Auto-Bewerben + Status-Leiste -->
<section class="card p-5 flex flex-col md:flex-row md:items-center gap-4 justify-between">
<div class="flex flex-col gap-1">
<div class="text-xs uppercase tracking-wide text-slate-500">Auto-Bewerben</div>
<div class="flex items-center gap-2">
{% if auto_apply_enabled %}
<span class="chip chip-warn">an</span>
<span class="text-sm text-slate-600">bei Match wird automatisch beworben</span>
{% else %}
<span class="chip chip-info">aus</span>
<span class="text-sm text-slate-600">Matches werden nur angezeigt</span>
{% endif %}
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<!-- der rote Knopf -->
<form method="post" action="/actions/auto-apply">
<input type="hidden" name="csrf" value="{{ csrf }}">
<input type="hidden" name="value" value="{% if auto_apply_enabled %}off{% else %}on{% endif %}">
<button class="btn btn-hot {% if not auto_apply_enabled %}off{% endif %}"
onclick="return confirm('{% if auto_apply_enabled %}Auto-Bewerben deaktivieren?{% else %}Auto-Bewerben aktivieren? Das System bewirbt dann automatisch bei jedem Match bitte Profil und Filter prüfen.{% endif %}');"
type="submit">
{% if auto_apply_enabled %}AUTO-BEWERBEN: AN{% else %}AUTO-BEWERBEN AKTIVIEREN{% endif %}
</button>
</form>
<!-- kill switch -->
<form method="post" action="/actions/kill-switch">
<input type="hidden" name="csrf" value="{{ csrf }}">
<input type="hidden" name="value" value="{% if kill_switch %}off{% else %}on{% endif %}">
<button class="btn {% if kill_switch %}btn-ghost{% else %}btn-danger{% endif %} text-sm" type="submit">
{% if kill_switch %}Kill-Switch deaktivieren{% else %}Kill-Switch{% endif %}
</button>
</form>
{% if circuit_open %}
<form method="post" action="/actions/reset-circuit">
<input type="hidden" name="csrf" value="{{ csrf }}">
<button class="btn btn-ghost text-sm" type="submit">Circuit zurücksetzen</button>
</form>
{% endif %}
</div>
</section>
{% if not apply_allowed %}
<div class="card p-4">
<span class="chip chip-bad">apply blockiert</span>
<span class="ml-2 text-sm text-slate-600">{{ apply_block_reason }}</span>
<div id="wohnungen-body"
hx-get="/partials/wohnungen"
hx-trigger="every 30s"
hx-swap="outerHTML">
{% include "_wohnungen_body.html" %}
</div>
{% endif %}
<!-- Status zeile -->
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-3">
<div class="text-xs text-slate-500">alert</div>
<div class="mt-1">
{% if last_alert_heartbeat %}<span class="chip chip-ok">live</span>
{% else %}<span class="chip chip-warn">kein Heartbeat</span>{% endif %}
</div>
</div>
<div class="card p-3">
<div class="text-xs text-slate-500">apply</div>
<div class="mt-1">
{% if apply_reachable %}<span class="chip chip-ok">ok</span>
{% else %}<span class="chip chip-bad">down</span>{% endif %}
</div>
</div>
<div class="card p-3">
<div class="text-xs text-slate-500">submit_forms</div>
<div class="mt-1">
{% if submit_forms %}<span class="chip chip-warn">echt senden</span>
{% else %}<span class="chip chip-info">dry-run</span>{% endif %}
</div>
</div>
<div class="card p-3">
<div class="text-xs text-slate-500">Fehler in Serie</div>
<div class="mt-1">
{% if circuit_open %}<span class="chip chip-bad">circuit open</span>
{% elif apply_failures > 0 %}<span class="chip chip-warn">{{ apply_failures }}</span>
{% else %}<span class="chip chip-ok">0</span>{% endif %}
</div>
</div>
</section>
<!-- Filter Panel -->
<details class="card" {% if not filters.rooms_min and not filters.max_rent %}open{% endif %}>
<summary class="px-5 py-3 font-semibold select-none">Eigene Filter</summary>
<form method="post" action="/actions/filters" class="p-5 grid grid-cols-2 md:grid-cols-3 gap-4">
<input type="hidden" name="csrf" value="{{ csrf }}">
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer min</label>
<input class="input" name="rooms_min" value="{{ filters.rooms_min if filters.rooms_min is not none else '' }}" placeholder="z.B. 2">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">Zimmer max</label>
<input class="input" name="rooms_max" value="{{ filters.rooms_max if filters.rooms_max is not none else '' }}" placeholder="z.B. 3">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">max Miete (€)</label>
<input class="input" name="max_rent" value="{{ filters.max_rent if filters.max_rent is not none else '' }}" placeholder="1500">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">min Größe (m²)</label>
<input class="input" name="min_size" value="{{ filters.min_size if filters.min_size is not none else '' }}" placeholder="40">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">max Anfahrt morgens (min)</label>
<input class="input" name="max_morning_commute" value="{{ filters.max_morning_commute if filters.max_morning_commute is not none else '' }}" placeholder="50">
</div>
<div>
<label class="block text-xs uppercase tracking-wide text-slate-500 mb-1">WBS benötigt</label>
<select class="input" name="wbs_required">
<option value="" {% if not filters.wbs_required %}selected{% endif %}>egal</option>
<option value="yes" {% if filters.wbs_required == 'yes' %}selected{% endif %}>ja</option>
<option value="no" {% if filters.wbs_required == 'no' %}selected{% endif %}>nein</option>
</select>
</div>
<div class="col-span-2 md:col-span-3 flex gap-2 pt-2">
<button class="btn btn-primary" type="submit">Filter speichern</button>
<span class="text-xs text-slate-500 self-center">Leer lassen = kein Limit. Filter bestimmen Match-Hervorhebung + Auto-Bewerben.</span>
</div>
</form>
</details>
<!-- Liste aller Wohnungen -->
<section class="card">
<div class="flex items-center justify-between px-4 py-3 border-b border-soft">
<h2 class="font-semibold">Neueste Wohnungen auf inberlinwohnen.de</h2>
<span class="text-xs text-slate-500">{{ flats|length }} gesamt</span>
</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 {% if item.matched %}bg-[#f2f8ff]{% endif %}">
<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.matched %}<span class="chip chip-ok">match</span>{% endif %}
{% if 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">apply fehlgeschlagen</span>
{% elif item.last %}<span class="chip chip-warn">läuft</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 {{ f.discovered_at }}
</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) %}
<form method="post" action="/actions/apply">
<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"
onclick="return confirm('Bewerbung für {{ (f.address or f.link)|e }} ausführen?');">
Bewerben
</button>
</form>
{% endif %}
</div>
</div>
{% else %}
<div class="px-4 py-8 text-center text-slate-500">Noch keine Wohnungen entdeckt.</div>
{% endfor %}
</div>
</section>
{% endblock %}