lazyflat/web/templates/wohnungen.html
Moritz c630b500ef multi-user: users, per-user profiles/filters/notifications, tab UI, apply forensics
* DB: users + user_profiles/filters/notifications/preferences; applications gets
  user_id + forensics_json + profile_snapshot_json; new errors table
  with 14d retention; schema versioning via MIGRATIONS list
* auth: password hashes in DB (argon2); env vars seed first admin; per-user
  sessions; CSRF bound to user id
* apply: personal info/WBS moved out of env into the request body; providers
  take an ApplyContext with Profile + submit_forms; full Playwright recorder
  (step log, console, page errors, network, screenshots, final HTML)
* web: five top-level tabs (Wohnungen/Bewerbungen/Logs/Fehler/Einstellungen);
  settings sub-tabs profil/filter/benachrichtigungen/account/benutzer;
  per-user matching, auto-apply and notifications (UI/Telegram/SMTP); red
  auto-apply switch on Wohnungen tab; forensics detail view for bewerbungen
  and fehler; retention background thread

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:52:41 +02:00

182 lines
9.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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>
{% 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 %}