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>
This commit is contained in:
parent
04b591fa9e
commit
7444f90d6a
16 changed files with 360 additions and 202 deletions
|
|
@ -5,62 +5,73 @@
|
|||
hx-trigger="every {{ poll_interval }}s"
|
||||
hx-swap="outerHTML">
|
||||
|
||||
<!-- Slim status strip: Alert · Filter · Auto-Bewerben · Trockenmodus -->
|
||||
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<!-- Alert -->
|
||||
<!-- 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">Alert</div>
|
||||
<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>
|
||||
|
||||
<!-- Filter summary -->
|
||||
<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>
|
||||
|
||||
<!-- Auto-Bewerben toggle -->
|
||||
<form class="card px-4 py-2.5 flex items-center justify-between gap-2"
|
||||
method="post" action="/actions/auto-apply"
|
||||
hx-post="/actions/auto-apply" hx-target="#wohnungen-body" hx-swap="outerHTML">
|
||||
<!-- 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 }}">
|
||||
<input type="hidden" name="value" value="{% if auto_apply_enabled %}off{% else %}on{% endif %}">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="text-[11px] uppercase tracking-wide text-slate-500">Auto-Bewerben</div>
|
||||
<div>{% if auto_apply_enabled %}<span class="chip chip-warn">aktiv</span>
|
||||
{% else %}<span class="chip chip-info">aus</span>{% endif %}</div>
|
||||
<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>
|
||||
<button class="btn {% if auto_apply_enabled %}btn-ghost{% else %}btn-hot{% endif %} text-xs"
|
||||
onclick="return confirm('{% if auto_apply_enabled %}Auto-Bewerben deaktivieren?{% else %}Auto-Bewerben aktivieren? Bei jedem passenden Flat wird automatisch beworben.{% endif %}');"
|
||||
type="submit">
|
||||
{% if auto_apply_enabled %}AUS{% else %}AN{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Trockenmodus toggle -->
|
||||
<form class="card px-4 py-2.5 flex items-center justify-between gap-2"
|
||||
method="post" action="/actions/submit-forms"
|
||||
hx-post="/actions/submit-forms" hx-target="#wohnungen-body" hx-swap="outerHTML">
|
||||
<!-- 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 }}">
|
||||
<input type="hidden" name="value" value="{% if submit_forms %}off{% else %}on{% endif %}">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="text-[11px] uppercase tracking-wide text-slate-500">Trockenmodus</div>
|
||||
<div>{% if submit_forms %}<span class="chip chip-warn">aus (echt!)</span>
|
||||
{% else %}<span class="chip chip-ok">an</span>{% endif %}</div>
|
||||
<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>
|
||||
<button class="btn btn-ghost text-xs"
|
||||
onclick="return confirm('{% if submit_forms %}Trockenmodus wieder einschalten?{% else %}Trockenmodus ausschalten? Formulare werden dann WIRKLICH abgesendet!{% endif %}');"
|
||||
type="submit">
|
||||
{% if submit_forms %}AN{% else %}AUS{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if not apply_allowed %}
|
||||
<div class="card p-3 text-sm">
|
||||
<span class="chip chip-bad">apply blockiert</span>
|
||||
<span class="chip chip-bad">Bewerbungs-Dienst nicht erreichbar</span>
|
||||
<span class="ml-2 text-slate-600">{{ apply_block_reason }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -68,8 +79,8 @@
|
|||
{% if circuit_open %}
|
||||
<div class="card p-3 text-sm flex items-center justify-between">
|
||||
<div>
|
||||
<span class="chip chip-bad">circuit open</span>
|
||||
<span class="ml-2 text-slate-600">{{ apply_failures }} Fehler in Serie — Auto-Bewerben pausiert</span>
|
||||
<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">
|
||||
|
|
@ -79,12 +90,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Liste aller Wohnungen -->
|
||||
<!-- 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">Neueste Wohnungen auf inberlinwohnen.de</h2>
|
||||
<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 }} gesehen</span>
|
||||
<span>{{ flats|length }} gefunden</span>
|
||||
{% if next_scrape_utc %}
|
||||
<span>· nächste Aktualisierung <span data-countdown-utc="{{ next_scrape_utc }}">…</span></span>
|
||||
{% endif %}
|
||||
|
|
@ -93,17 +104,16 @@
|
|||
<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="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.matched %}<span class="chip chip-ok">match</span>{% endif %}
|
||||
{% 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">apply fehlgeschlagen</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">
|
||||
|
|
@ -128,7 +138,7 @@
|
|||
<input type="hidden" name="flat_id" value="{{ f.id }}">
|
||||
<button class="btn btn-primary text-sm" type="submit"
|
||||
{% if is_running %}disabled{% endif %}
|
||||
onclick="return confirm('Bewerbung für {{ (f.address or f.link)|e }} ausführen?');">
|
||||
hx-confirm="Bewerbung für {{ (f.address or f.link)|e }} starten?">
|
||||
{% if is_running %}läuft…{% else %}Bewerben{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -136,7 +146,13 @@
|
|||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-4 py-8 text-center text-slate-500">Noch keine Wohnungen entdeckt.</div>
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue