Codex-Handover P0: Daten-Ehrlichkeit (Seed=Demo statt 'live/verfügbar', Fake-Namen+Mojibake raus), Finder modell-Param, LIVE_URL via Env, 404-Seite+Favicon, Umami nur mit echter ID, ehrliche Texte/Semantik, Demo-CTA entschärft

This commit is contained in:
2026-06-28 16:32:06 +00:00
parent fba01a70bb
commit ed41ea54d2
9 changed files with 50 additions and 29 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#1f5a62"/><g fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round"><path d="M32 14v36M16 23l32 18M48 23 16 41"/></g></svg>

After

Width:  |  Height:  |  Size: 238 B

+2 -2
View File
@@ -11,12 +11,12 @@ import '../styles/global.css';
<title>{title}</title>
<meta name="description" content={desc} />
<link rel="canonical" href={Astro.url.href} />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/favicon.svg" />
<meta property="og:title" content={title} />
<meta property="og:description" content={desc} />
<meta property="og:type" content="website" />
{jsonld && <script type="application/ld+json" set:html={JSON.stringify(jsonld)} />}
<!-- Umami Analytics (self-hosted). TODO: data-website-id setzen, Host prüfen. -->
<script defer is:inline src="https://umami.heidrich-digital.de/script.js" data-website-id="REPLACE_WITH_UMAMI_WEBSITE_ID" data-domains="kuehlfinder.de"></script>
</head>
<body>
<header class="site"><div class="wrap bar">
+15
View File
@@ -0,0 +1,15 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="Seite nicht gefunden Kühlfinder" description="Diese Seite gibt es nicht.">
<section class="wrap" style="margin:3rem auto;max-width:560px;text-align:center">
<span class="eyebrow">404</span>
<h1>Seite nicht gefunden</h1>
<p class="muted">Diese Seite existiert nicht (mehr). Weiter geht's hier:</p>
<p style="display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap;margin-top:1rem">
<a class="btn" href="/karte">Zur Live-Karte</a>
<a class="btn ghost" href="/">Startseite</a>
<a class="btn ghost" href="/finder">Finder</a>
</p>
</section>
</Base>
+1 -1
View File
@@ -50,7 +50,7 @@ const products = allProducts.map(p=>({slug:p.slug,name:p.name,type:p.type,badge:
<div class="reco"><span class="badge">Budget</span>${card(r.budget)}</div>
${r.warn?`<p class="warn">${r.warn}</p>`:''}
<div class="rcta">
<a class="btn" href="/karte?produkt=${r.primary.slug}">Verfügbarkeit auf Karte</a>
<a class="btn" href="/karte?modell=${r.primary.slug}">Verfügbarkeit auf Karte</a>
<a class="btn ghost" href="/alarm?produkt=${r.primary.slug}">Alarm setzen</a>
<button class="btn ghost" id="again">Nochmal</button>
</div>`;
+12 -9
View File
@@ -15,7 +15,7 @@ const Bell='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="
<WeatherBanner />
<span class="eyebrow" style="margin-top:1rem"><span class="dot ok live"></span>Live-Verfügbarkeit · eigener Scraper</span>
<h1>Wo gibt es gerade ein&nbsp;Klimagerät?</h1>
<p class="lead">Bei Hitze ist alles vergriffen. Kühlfinder ist dein Kontrollzentrum: <strong>live sehen, welcher Markt in der Nähe eins auf Lager hat</strong> Baumärkte, Elektromärkte, Online &amp; eBay mit Prognose, Preis-Check gegen Wucher und Alarm. <em>Die nächste Hitzewelle kommt bestimmt.</em></p>
<p class="lead">Bei Hitze ist alles vergriffen. Kühlfinder prüft <strong>Preise live direkt bei Händlern</strong> und zeigt ehrlich, wo geprüft ist plus Standort-Abdeckung, Preis-Check gegen Wucher und Alarm. <em>Die nächste Hitzewelle kommt bestimmt.</em></p>
<form class="herobar" action="/karte" method="get">
<input name="plz" inputmode="numeric" placeholder="PLZ oder Ort …" aria-label="PLZ oder Ort" />
<button class="btn" type="submit" data-umami-event="hero_plz_suche">Verfügbarkeit zeigen</button>
@@ -23,12 +23,12 @@ const Bell='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="
</form>
<div class="livewrap reveal">
<div class="pills">
<span class="pill-stat"><b class="num" id="p-ac" data-count="0">…</b>&nbsp;Märkte mit Klimagerät jetzt</span>
<span class="pill-stat"><b class="num" id="p-total" data-count="0">…</b>&nbsp;Märkte überwacht</span>
<span class="pill-stat"><b class="num" id="p-ac">…</b>&nbsp;Live-Preise geprüft</span>
<span class="pill-stat"><b class="num" id="p-total" data-count="0">…</b>&nbsp;Märkte (Abdeckung)</span>
<span class="pill-stat"><b class="num">6</b>&nbsp;Länder · <span id="p-chains">…</span> Ketten</span>
</div>
<div class="livecard card">
<div class="lc-head"><span class="dot ok live"></span> Gerade verfügbar</div>
<div class="lc-head"><span class="dot ok live"></span> Live geprüfte Preise</div>
<ul class="livelist" id="livelist"><li class="skel" style="height:40px"></li><li class="skel" style="height:40px"></li><li class="skel" style="height:40px"></li></ul>
<a class="btn sm" href="/karte">Alle auf der Karte →</a>
</div>
@@ -122,13 +122,16 @@ const Bell='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="
<script>
const euro=(n,c)=>n==null?'':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
document.getElementById('locbtn')?.addEventListener('click',e=>{ if(navigator.geolocation){ /* Karte hat eigene Standortfunktion */ } });
const CRAWLER=(import.meta.env.PUBLIC_CRAWLER_URL)||'http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{
const ac=document.getElementById('p-ac'), tot=document.getElementById('p-total');
ac.setAttribute('data-count', d.stats.withAC); ac.removeAttribute('data-done');
const tot=document.getElementById('p-total');
tot.setAttribute('data-count', d.stats.storesTotal); tot.removeAttribute('data-done');
const chains=new Set(d.stores.map(s=>s.c)); document.getElementById('p-chains').textContent=chains.size;
document.getElementById('p-chains').textContent=new Set(d.stores.map(s=>s.c)).size;
window.kfAnim && window.kfAnim();
const avail=d.stores.filter(s=>s.any.a && s.any.minP).sort(()=>Math.random()-0.5).slice(0,6);
document.getElementById('livelist').innerHTML = avail.map(s=>`<li><span><span class="flag">${s.flag||''}</span> <span class="lm">${s.c}</span> <span class="lc">${s.ci}</span></span><span class="lp">${euro(s.any.minP,s.cur)}</span></li>`).join('') || '<li class="lc">Aktuell sehr knapp Alarm setzen lohnt sich.</li>';
}).catch(()=>{});
fetch(CRAWLER+'/api/live').then(r=>r.json()).then(d=>{
const offers=(d.offers||[]).filter(o=>o.price);
document.getElementById('p-ac').textContent=offers.length;
document.getElementById('livelist').innerHTML = offers.slice(0,6).map(o=>`<li><span><span class="lm">${o.chain}</span> <span class="lc">${o.name}</span></span><span class="lp">${euro(o.price)}</span></li>`).join('') || '<li class="lc">Crawler startet gleich wieder reinschauen.</li>';
}).catch(()=>{document.getElementById('p-ac').textContent='';document.getElementById('livelist').innerHTML='<li class="lc">Live-Preise gerade nicht erreichbar.</li>';});
</script>
+15 -13
View File
@@ -11,7 +11,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<WeatherBanner />
<div class="mon-hero-row" style="margin-top:1rem">
<div>
<span class="eyebrow"><span class="dot ok live"></span>Live · eigener Scraper · 6 Länder</span>
<span class="eyebrow"><span class="dot ok live"></span>Live-Preis-Crawler · Standort-Abdeckung (Demo) · 6 Länder</span>
<h1>Dein Klimagerät-Kontrollzentrum</h1>
<p class="lead">Live-Bestand in Baumärkten, Elektromärkten, Online &amp; eBay mit <strong>Verfügbarkeits-Prognose</strong> und <strong>Preis-Check gegen Wucher</strong>.</p>
</div>
@@ -19,11 +19,11 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
</div>
<div class="delaybar reveal">
<span class="dot ok live"></span>
<span>Kostenlos siehst du Live-Daten <strong>~10 Min zeitversetzt</strong>. <a href="/pro">Mit Pro in Echtzeit + Push →</a></span>
<span><strong>Filialbestände sind Demo-Abdeckung (nicht verifiziert).</strong> Verifizierte <strong>Live-Preise</strong> (eigener Crawler) stehen unten in der Preis-Leiste. <a href="/pro">Pro: Echtzeit + Alarm →</a></span>
</div>
<div class="stats" id="stats">
<div class="stat"><span class="note">Märkte</span><b class="num" data-k="storesTotal"></b></div>
<div class="stat ok"><span class="note">mit Klimagerät</span><b class="num" data-k="withAC"></b></div>
<div class="stat"><span class="note">Märkte (Abdeckung)</span><b class="num" data-k="storesTotal"></b></div>
<div class="stat ok"><span class="note">Live-Preise geprüft</span><b class="num" id="s-live"></b></div>
<div class="stat"><span class="note">Baumärkte</span><b class="num" data-k="baumarkt"></b></div>
<div class="stat"><span class="note">Elektromärkte</span><b class="num" data-k="elektro"></b></div>
<div class="stat"><span class="note">Länder</span><b class="num" data-k="countries"></b></div>
@@ -52,7 +52,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<label class="rad">Umkreis <span class="num" id="r-out"></span><input id="radius" type="range" min="10" max="500" step="10" value="500" disabled /></label>
</div>
<div class="tb-row chips">
<button class="chip ghost on" data-toggle="avail">Nur verfügbare</button>
<button class="chip ghost" data-toggle="avail">Nur verfügbare (live)</button>
<button class="chip ghost" data-toggle="chance">Beste Prognose</button>
<span class="sep"></span>
<button class="chip on" data-kind="all">Alle Typen</button>
@@ -79,7 +79,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<div><h2 class="h-sm">Nichts in der Nähe?</h2><p class="note" style="margin:0">Verfügbarkeits-Alarm + Preiswecker Mail &amp; Telegram, mit Prognose-Frühwarnung.</p></div>
<a class="btn" href="/alarm">Alarm &amp; Preiswecker</a>
</div>
<p class="note" style="margin-top:.6rem">Datenquelle: <strong>eigener Kühlfinder-Scraper</strong> (Händler direkt) keine Drittdaten. Aktuell Seed-Stand; Live-Crawler + echte Historie folgen. Prognose = statistische Schätzung. Quellen: Wetter <a href="https://open-meteo.com/" rel="nofollow">Open-Meteo</a>, Hitze/Gesundheit <a href="https://www.umweltbundesamt.de/themen/gesundheit/umwelteinfluesse-auf-den-menschen/hitze" rel="nofollow">UBA</a>.</p>
<p class="note" style="margin-top:.6rem">Standort-Abdeckung ist <strong>Demo</strong> (keine verifizierten Filialbestände). <strong>Verifizierte Live-Preise</strong> kommen vom eigenen Crawler (ausgewählte Produktseiten). Filial-Live folgt mit Händler-Anbindung. Prognose = statistische Schätzung. Quellen: Wetter <a href="https://open-meteo.com/" rel="nofollow">Open-Meteo</a>, Hitze/Gesundheit <a href="https://www.umweltbundesamt.de/themen/gesundheit/umwelteinfluesse-auf-den-menschen/hitze" rel="nofollow">UBA</a>.</p>
</section>
</Base>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
@@ -143,11 +143,11 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<script>
import maplibregl from 'maplibre-gl';
const FLAG={DE:'🇩🇪',AT:'🇦🇹',CH:'🇨🇭',NL:'🇳🇱',PL:'🇵🇱',DK:'🇩🇰'};
const LIVE_URL='http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
const LIVE_URL=(import.meta.env.PUBLIC_CRAWLER_URL)||'http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
const fmt=(n,c)=>n==null?'':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
const $=s=>document.querySelector(s),$$=s=>Array.from(document.querySelectorAll(s));
let DATA=null,center=null,shown=120;
const state={q:'',model:'all',countries:new Set(),kind:'all',avail:true,chance:false,radius:500};
const state={q:'',model:'all',countries:new Set(),kind:'all',avail:false,chance:false,radius:500};
const params=new URLSearchParams(location.search);
if(params.get('modell'))state.model=params.get('modell');
if(params.get('plz'))state.q=params.get('plz');
@@ -191,10 +191,11 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
const tb=$('#rows');tb.innerHTML='';
for(const s of list.slice(0,shown)){
const st=status(s);
const stk = st.a?`<span class="stk ok"><span class="dot ok"></span> ${state.model==='all'?st.n+' verfügbar':'verfügbar'}</span>`:`<span class="stk off"><span class="dot off"></span> vergriffen</span>`;
const stk = st.a?`<span class="stk ok"><span class="dot ok"></span> live verfügbar</span>`:`<span class="stk off"><span class="dot off"></span> Demo · beim Händler prüfen</span>`;
const distTd=center?`<td class="ce num">${dist(center[1],center[0],s.la,s.lo).toFixed(0)} km</td>`:'';
tb.insertAdjacentHTML('beforeend',`<tr><td><span class="flag">${s.flag||FLAG[s.cc]||''}</span> <span class="fname">${s.n}</span><span class="kindb">${s.kind==='elektro'?'Elektro':'Baumarkt'}</span><div class="faddr">${s.z} ${s.ci} · <span title="zuletzt geprüft">Stand vor ${s.t||'?'} Min</span></div></td>
<td class="ce">${stk}</td><td class="ce num">${fmt(st.p,s.cur)}</td>
const surl="https://www.google.com/search?q="+encodeURIComponent(s.c+" "+s.ci+" Klimagerät");
tb.insertAdjacentHTML('beforeend',`<tr><td><span class="flag">${s.flag||FLAG[s.cc]||''}</span> <a class="fname" href="${surl}" target="_blank" rel="noopener">${s.n}</a><span class="kindb">${s.kind==='elektro'?'Elektro':'Baumarkt'}</span><div class="faddr">${s.z} ${s.ci} · <span title="zuletzt geprüft">Stand vor ${s.t||'?'} Min</span></div></td>
<td class="ce">${stk}</td><td class="ce num" title="Referenzpreis (Demo)">${st.p?fmt(st.p,s.cur):""}</td>
<td class="ce"><span class="prog"><span class="progbar"><i style="width:${st.q||0}%"></i></span><span class="num">${st.q||0}%</span></span></td>${distTd}</tr>`);
}
$('#rowcount').textContent=`${Math.min(shown,list.length)} von ${list.length} Märkten`;
@@ -204,7 +205,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
function fillStatic(){
$('#h-updated').textContent=DATA.stats.updated;
$$('#stats b[data-k]').forEach(b=>b.textContent=(DATA.stats[b.dataset.k]??'').toLocaleString('de-DE'));
$('#s-wucher').textContent=DATA.online.filter(o=>o.wucher).length;
$('#s-wucher').textContent=(DATA.online||[]).filter(o=>o.wucher).length;
$('#h-feednote').textContent='Feed live';
$('#feed').innerHTML=DATA.feed.slice(0,18).map(f=>`<li><div class="fl"><b>${FLAG[f.cc]||''} ${f.name}</b><span>${f.model}</span></div><span class="ft">${rel(f.at)}</span></li>`).join('');
$('#onlinebar').innerHTML='<span class="note" style="align-self:center">Online &amp; eBay:</span>'+DATA.online.map(o=>`<a class="osrc ${o.wucher?'wucher':''}" href="${o.url}" target="_blank" rel="nofollow noopener"><b>${o.src}${o.wucher?' ⚠':''}</b><span class="num">${fmt(o.p)}</span><small>${o.cond?o.cond+' · ':''}${o.a?(o.wucher?'Wucher':'verfügbar'):'nicht verf.'}</small><small class="trust" title="Seriosität">${stars(o.trust||3)}</small></a>`).join('');
@@ -221,11 +222,12 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
const when=d.generatedAt?new Date(d.generatedAt):null;
const mins=when?Math.max(1,Math.round((Date.now()-when)/60000)):'?';
const items=d.offers.filter(o=>o.price||o.availability==='discontinued').map(o=>{
const av=o.availability==='in_stock'?'im Markt':(o.availability==='online_available'?'online':(o.availability==='discontinued'?'⚠ Seite entfernt (404)':'vergriffen'));
const av=o.availability==='in_stock'?'Produktseite: verfügbar':(o.availability==='online_available'?'online verfügbar':(o.availability==='discontinued'?'⚠ Seite entfernt (404)':(o.availability==='blocked'?'blockiert':'vergriffen')));
const cls=(o.availability==='out_of_stock')?'':'';
return `<a class="osrc ${o.availability==='discontinued'?'wucher':''}" href="${o.url}" target="_blank" rel="sponsored nofollow noopener"><b>${o.chain} · LIVE</b><span class="num">${fmt(o.price)}</span><small>${o.name} · ${av}</small></a>`;
}).join('');
const bar=$('#onlinebar');
const liveN=d.offers.filter(o=>o.price).length; var le=document.getElementById('s-live'); if(le)le.textContent=liveN;
const note = d.blocked?` · ${d.blocked} Quelle(n) blockiert (Proxy nötig)`:'';
bar.insertAdjacentHTML('afterbegin', `<span class="livebadge" title="eigener Live-Crawler"><span class="dot ok live"></span> LIVE gescrapt · Stand vor ${mins} Min${note}</span>`+items);
}).catch(()=>{});
+2 -2
View File
@@ -35,8 +35,8 @@ const jsonld={'@context':'https://schema.org','@type':'FAQPage',mainEntity:[
<li>★ <strong>Preiswecker</strong>: Alarm, wenn Preis unter dein Limit fällt</li>
<li>★ Beste-Chancen-Route in deinem Umkreis</li>
</ul>
<a class="btn" href="/alarm" data-umami-event="pro_pass_cta">Pro-Pass holen</a>
<p class="note" style="margin:.5rem 0 0">Zahlung per <strong>Mollie</strong> (EU, NL) iDEAL, SEPA, Karte, Apple/Google Pay. Folgt. Keine automatische Verlängerung.</p>
<a class="btn" href="/alarm" data-umami-event="pro_pass_cta">Auf die Warteliste</a>
<p class="note" style="margin:.5rem 0 0">Vorschau Bezahlung per <strong>Mollie</strong> (EU) kommt zum Verkaufsstart. Noch nichts zu zahlen, keine automatische Verlängerung.</p>
</div>
</div>