* Wohnungen header: single slim row with Alert status · Filter summary · Auto-Bewerben toggle · Trockenmodus toggle. Big filter panel removed — filters live only in /einstellungen/filter. * Alert status: 'nicht eingerichtet' until the user has actual filters (+ valid notification creds if telegram/email). 'aktiv' otherwise. * Logs tab: admin-only (gated both in layout and server-side). Shows merged audit + errors across all users, sorted newest-first, capped at 300. * Apply, auto-apply, trockenmodus and circuit reset buttons post via HTMX and swap the Wohnungen body. While any application is still running for the user the poll interval drops from 30s to 3s so status flips to 'beworben' or 'fehlgeschlagen' almost immediately. * Browser tab title is now always 'lazyflat'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
7.6 KiB
HTML
144 lines
7.6 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">
|
|
|
|
<!-- Slim status strip: Alert · Filter · Auto-Bewerben · Trockenmodus -->
|
|
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<!-- Alert -->
|
|
<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="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>
|
|
|
|
<!-- 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">
|
|
<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>
|
|
<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">
|
|
<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>
|
|
<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="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">circuit open</span>
|
|
<span class="ml-2 text-slate-600">{{ apply_failures }} Fehler in Serie — Auto-Bewerben pausiert</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 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>
|
|
{% 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.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>
|
|
{% 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 %}
|
|
onclick="return confirm('Bewerbung für {{ (f.address or f.link)|e }} ausführen?');">
|
|
{% 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">Noch keine Wohnungen entdeckt.</div>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|