lazyflat/web/templates/_wohnung_detail.html
EiSiMo eb66284172 enrichment: Haiku flat details + image gallery on expand
apply service
- POST /internal/fetch-listing: headless Playwright fetch of a listing URL,
  returns {html, image_urls[], final_url}. Uses the same browser
  fingerprint/profile as the apply run so bot guards don't kick in

web service
- New enrichment pipeline (web/enrichment.py):
  /internal/flats → upsert → kick() enrichment in a background thread
    1. POST /internal/fetch-listing on apply
    2. llm.extract_flat_details(html, url) — Haiku tool-use call returns
       structured JSON (address, rooms, rent, description, pros/cons, etc.)
    3. Download each image directly to /data/flats/<slug>/NN.<ext>
    4. Persist enrichment_json + image_count + enrichment_status on the flat
- llm.py: minimal Anthropic /v1/messages wrapper, no SDK
- DB migration v5 adds enrichment_json/_status/_updated_at + image_count
- Admin "Altbestand anreichern" button (POST /actions/enrich-all) queues
  backfill for all pending/failed rows; runs in a detached task
- GET /partials/wohnung/<id> renders _wohnung_detail.html
- GET /flat-images/<slug>/<n> serves the downloaded image

UI
- Chevron on each list row toggles an inline detail pane (HTMX fetch on
  first open, hx-preserve keeps it open across the 3–30 s polls)
- CSS .flat-gallery normalises image tiles to a 4/3 aspect with object-fit:
  cover so different source sizes align cleanly
- "analysiert…" / "?" chips on the list reflect enrichment_status

Config
- ANTHROPIC_API_KEY + ANTHROPIC_MODEL wired into docker-compose's web
  service (default model: claude-haiku-4-5-20251001)

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

82 lines
3.5 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.

{# Expanded detail for a single flat, loaded into #flat-detail-<id> via HTMX. #}
{% if enrichment_status == 'pending' %}
<div class="px-4 py-5 text-sm text-slate-500">Analyse läuft kommt in wenigen Augenblicken zurück…</div>
{% elif enrichment_status == 'failed' %}
<div class="px-4 py-5 text-sm text-slate-500">
Detail-Analyse konnte nicht abgerufen werden.
<a href="{{ flat.link }}" target="_blank" rel="noopener">Zur Original-Anzeige →</a>
</div>
{% else %}
<div class="px-4 py-4 space-y-4">
{% if image_urls %}
<div class="flat-gallery">
{% for src in image_urls %}
<a class="flat-gallery-tile" href="{{ src }}" target="_blank" rel="noopener">
<img src="{{ src }}" loading="lazy" alt="Foto {{ loop.index }}">
</a>
{% endfor %}
</div>
{% endif %}
{% if enrichment and enrichment.description %}
<p class="text-sm text-slate-700">{{ enrichment.description }}</p>
{% endif %}
{% if enrichment %}
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-1.5 text-xs">
{% macro kv(label, value) %}
{% if value is not none and value != '' %}
<div class="flex justify-between gap-3 border-b border-soft py-1">
<span class="text-slate-500">{{ label }}</span>
<span class="text-slate-800 text-right">{{ value }}</span>
</div>
{% endif %}
{% endmacro %}
{{ kv('Adresse', enrichment.address) }}
{{ kv('Zimmer', enrichment.rooms) }}
{{ kv('Größe', enrichment.size_sqm ~ ' m²' if enrichment.size_sqm else none) }}
{{ kv('Kaltmiete', enrichment.rent_cold ~ ' €' if enrichment.rent_cold else none) }}
{{ kv('Nebenkosten', enrichment.utilities ~ ' €' if enrichment.utilities else none) }}
{{ kv('Gesamtmiete', enrichment.rent_total ~ ' €' if enrichment.rent_total else none) }}
{{ kv('Kaution', enrichment.deposit ~ ' €' if enrichment.deposit else none) }}
{{ kv('Bezugsfrei ab', enrichment.available_from) }}
{{ kv('Etage', enrichment.floor) }}
{{ kv('Heizung', enrichment.heating) }}
{{ kv('Energieausweis', enrichment.energy_certificate) }}
{{ kv('Energiewert', enrichment.energy_value) }}
{{ kv('Baujahr', enrichment.year_built) }}
{{ kv('WBS', 'erforderlich' if enrichment.wbs_required else ('nicht erforderlich' if enrichment.wbs_required == false else none)) }}
{{ kv('WBS-Typ', enrichment.wbs_type) }}
</div>
{% endif %}
{% if enrichment and enrichment.features %}
<div class="flex flex-wrap gap-1.5">
{% for f in enrichment.features %}<span class="chip chip-info">{{ f }}</span>{% endfor %}
</div>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% if enrichment and enrichment.pros %}
<div>
<div class="text-xs uppercase tracking-wide text-slate-500 mb-1">Pro</div>
<ul class="text-sm space-y-1">
{% for p in enrichment.pros %}<li>+ {{ p }}</li>{% endfor %}
</ul>
</div>
{% endif %}
{% if enrichment and enrichment.cons %}
<div>
<div class="text-xs uppercase tracking-wide text-slate-500 mb-1">Contra</div>
<ul class="text-sm space-y-1">
{% for c in enrichment.cons %}<li> {{ c }}</li>{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="text-xs">
<a href="{{ flat.link }}" target="_blank" rel="noopener">Zur Original-Anzeige →</a>
</div>
</div>
{% endif %}