-
@@ -119,6 +118,8 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
.pcell{display:flex;flex-direction:column}.pcell .num{font-size:1.05rem;font-weight:600}.pcell small{font-size:.72rem;color:var(--slate)}
.pcell.ok .num{color:var(--ok-dot)}.pcell.bad .num{color:var(--danger)}
.onlinebar{margin-top:.8rem;display:flex;gap:.5rem;flex-wrap:wrap;align-items:stretch;padding:.7rem .9rem}
+ .onlinebar .offer-card{flex:1 1 210px;max-width:280px}
+ @media(max-width:560px){.onlinebar .offer-card{flex:1 1 100%;max-width:none}}
.osrc{display:flex;flex-direction:column;gap:.1rem;border:1px solid var(--line);border-radius:10px;padding:.45rem .65rem;min-width:128px}
.osrc b{font-size:.82rem}.osrc .num{font-size:.92rem}.osrc small{font-size:.71rem;color:var(--slate)}
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}.osrc.wucher .num{color:var(--danger)}
@@ -162,6 +163,8 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
const storeLink=(s)=>{ const q=(state.model!=='all'&&DATA.products[state.model]?DATA.products[state.model].name:'Klimagerät'); const fn=CHAIN_SEARCH[s.c]; return fn?fn(q):('https://www.google.com/search?q='+encodeURIComponent(s.c+' '+s.ci+' '+q)); };
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 esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+ const safeUrl=u=>{try{const x=new URL(u,location.origin);return (x.protocol==='http:'||x.protocol==='https:')?x.href:'#';}catch(e){return '#';}};
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:false,chance:false,radius:500};
@@ -208,11 +211,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?`
live verfügbar`:`
beim Händler prüfen ↗`;
+ const surl=safeUrl(storeLink(s));
+ const stk = `
beim Händler prüfen ↗`;
const distTd=center?`
${dist(center[1],center[0],s.la,s.lo).toFixed(0)} km | `:'';
- const surl=storeLink(s);
- tb.insertAdjacentHTML('beforeend',`
| ${s.flag||FLAG[s.cc]||''} ${s.n}${s.kind==='elektro'?'Elektro':'Baumarkt'} ${s.z} ${s.ci} · Stand vor ${s.t||'?'} Min |
- ${stk} | ${st.p?fmt(st.p,s.cur):"–"} |
+ tb.insertAdjacentHTML('beforeend',`
| ${s.flag||FLAG[s.cc]||''} ${esc(s.n)}${s.kind==='elektro'?'Elektro':'Baumarkt'} ${esc(s.z)} ${esc(s.ci)} · Demo-Standort, Filialbestand nicht verifiziert |
+ ${stk} | – |
${st.q||0}% | ${distTd}
`);
}
$('#rowcount').textContent=`${Math.min(shown,list.length)} von ${list.length} Märkten`;
@@ -222,10 +225,13 @@ 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;
- $('#h-feednote').textContent='Feed live';
- $('#feed').innerHTML=DATA.feed.slice(0,18).map(f=>`
${FLAG[f.cc]||''} ${f.name}${f.model}
${rel(f.at)}`).join('');
- $('#onlinebar').innerHTML='
Online & eBay:'+DATA.online.map(o=>`
${o.src}${o.wucher?' ⚠':''}${fmt(o.p)}${o.cond?o.cond+' · ':''}${o.a?(o.wucher?'Wucher':'verfügbar'):'nicht verf.'}${stars(o.trust||3)}`).join('');
+ $('#s-wucher').textContent=(DATA.online||[]).filter(o=>o.wucher).length||'–';
+ $('#h-feednote').textContent='Standort-Abdeckung (Demo)';
+ const feed=DATA.feed||[];
+ $('#feed').innerHTML = feed.length
+ ? feed.slice(0,18).map(f=>`
${FLAG[f.cc]||''} ${esc(f.name)}${esc(f.model)}
${rel(f.at)}`).join('')
+ : '
Noch keine verifizierten Restock-Ereignisse. Sobald der Crawler eine Wiederverfügbarkeit erkennt, erscheint sie hier.';
+ // Online-Leiste wird von loadLive() mit echten Live-Angeboten gefüllt
const ccs=[...new Set(DATA.stores.map(s=>s.cc))];
$('#countrychips').innerHTML=ccs.map(c=>`
`).join(' ');
$$('#countrychips .chip').forEach(b=>b.addEventListener('click',()=>{const c=b.dataset.cc;state.countries.has(c)?(state.countries.delete(c),b.classList.remove('on')):(state.countries.add(c),b.classList.add('on'));render();}));
@@ -234,20 +240,30 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
loadLive();
}
function loadLive(){
+ const bar=$('#onlinebar');
fetch(LIVE_URL+'/api/live').then(r=>r.json()).then(d=>{
- if(!d||!d.offers||!d.offers.length)return;
- const when=d.generatedAt?new Date(d.generatedAt):null;
+ const offers=(d&&d.offers)||[];
+ const when=d&&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'?'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 `
${o.chain} · LIVE${fmt(o.price)}${o.name} · ${av}`;
+ const liveN=offers.filter(o=>o.price).length; const le=document.getElementById('s-live'); if(le)le.textContent=liveN||'–';
+ const shownOffers=offers.filter(o=>o.price||o.availability==='discontinued')
+ .sort((a,b)=>(a.price||1e9)-(b.price||1e9));
+ const items=shownOffers.map(o=>{
+ let dot='status-ok',meta='Produktseite geprüft',alert='';
+ if(o.availability==='online_available'){meta='Online-Angebot gefunden';}
+ else if(o.availability==='in_stock'){meta='Produktseite: Angebot vorhanden';}
+ else if(o.availability==='discontinued'){dot='status-bad';meta='⚠ Produktseite entfernt (404)';alert=' is-alert';}
+ else if(o.availability==='out_of_stock'){dot='status-mut';meta='derzeit vergriffen';}
+ const price=o.price?fmt(o.price):'—';
+ return `
`
+ +`${esc(o.chain)}${price}`
+ +`${esc(o.name)}`
+ +`${meta} · vor ${mins} Min`;
}).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', `
LIVE gescrapt · Stand vor ${mins} Min${note}`+items);
- }).catch(()=>{});
+ const note = d&&d.blocked?` · ${d.blocked} Quelle(n) blockiert`:'';
+ const head=`
LIVE-Preise gescrapt · Stand vor ${mins} Min${note}`;
+ bar.innerHTML = items ? (head+items) : '
Live-Preise gerade nicht erreichbar – bitte gleich erneut schauen.';
+ }).catch(()=>{bar.innerHTML='
Live-Preise gerade nicht erreichbar.';});
}
map.on('load',()=>{
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
@@ -256,7 +272,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
map.addLayer({id:'pt',type:'circle',source:'all',filter:['!',['has','point_count']],paint:{'circle-color':'#2b6a72','circle-opacity':.6,'circle-radius':4,'circle-stroke-width':1,'circle-stroke-color':'#fff'}});
map.addSource('av',{type:'geojson',data:{type:'FeatureCollection',features:[]}});
map.addLayer({id:'av',type:'circle',source:'av',paint:{'circle-color':'#1d9e75','circle-radius':7,'circle-stroke-width':2,'circle-stroke-color':'#fff'}});
- map.on('click','av',e=>{const p=e.features[0].properties;new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`
${p.n}${p.ci} – verfügbar
Stand vor ${p.t||'?'} Min`).addTo(map);});
+ map.on('click','av',e=>{const p=e.features[0].properties;new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`
${p.n}${p.ci} · Demo-Standort
Filialbestand nicht verifiziert`).addTo(map);});
map.on('click','cl',e=>{const f=map.queryRenderedFeatures(e.point,{layers:['cl']});map.getSource('all').getClusterExpansionZoom(f[0].properties.cluster_id).then(z=>map.easeTo({center:f[0].geometry.coordinates,zoom:z}));});
if(DATA)render();
});
diff --git a/src/pages/llms-full.txt.ts b/src/pages/llms-full.txt.ts
index 8e1b5dc..65a9ee5 100644
--- a/src/pages/llms-full.txt.ts
+++ b/src/pages/llms-full.txt.ts
@@ -2,7 +2,7 @@ import { allProducts, priceIntel, euro } from '../lib/pricing';
export const GET = () => {
const L:string[] = [
'# Kühlfinder – Vollständige Referenz',
- '> Kontrollzentrum für Klimagerät-Verfügbarkeit in DE/AT/CH/NL/PL/DK: Live-Bestand in Bau- und Elektromärkten + Online/Amazon/eBay, Verfügbarkeits-Prognose, Preis-Check mit Wucher-Warnung, Custom-Alarm + Preiswecker. Eigener Scraper, keine Drittdaten.',
+ '> Kontrollzentrum für Klimageräte in DE/AT/CH/NL/PL/DK: Live-Preise direkt bei Händlern (eigener Crawler, ausgewählte Produktseiten) + Standort-Abdeckung, Verfügbarkeits-Prognose, Preis-Check mit Wucher-Warnung, Custom-Alarm + Preiswecker. Filialbestände sind noch nicht live verifiziert.',
'',
'## Kernfunktionen',
'- Karte & Tabelle mit Standort-Abdeckung (Demo) + live geprüften Online-Preisen ausgewählter Produktseiten.',
diff --git a/src/styles/global.css b/src/styles/global.css
index a506637..d65fd6d 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -118,3 +118,19 @@ header.site .brand{letter-spacing:-.02em}
.anim-in{animation:kf-in .45s cubic-bezier(.16,1,.3,1) both}
.flag{font-size:1.05em;line-height:1}
@media (prefers-reduced-motion: reduce){.reveal{opacity:1!important;transform:none!important}.dot.live{animation:none}}
+
+/* --- Offer-Cards (Live-Angebote): Quelle/Preis/Name/Status sauber getrennt, ganze Karte klickbar --- */
+.offer-card{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.2rem .9rem;align-content:start;text-decoration:none;color:var(--ink);border:1px solid var(--line);border-radius:12px;padding:.6rem .8rem;background:var(--card);box-shadow:var(--shadow-card);transition:border-color .15s,transform .1s}
+.offer-card:hover{border-color:var(--petrol);transform:translateY(-1px)}
+.offer-card:focus-visible{outline:2px solid var(--petrol);outline-offset:2px}
+.offer-source{font-weight:600;min-width:0;overflow-wrap:anywhere}
+.offer-price{font-family:var(--mono);font-weight:700;white-space:nowrap;text-align:right;color:var(--petrol-deep)}
+.offer-title{grid-column:1 / -1;color:var(--ink-2);font-size:.82rem;line-height:1.35;overflow-wrap:anywhere}
+.offer-meta{grid-column:1 / -1;color:var(--slate);font-size:.74rem;display:inline-flex;gap:.4rem;align-items:center}
+.offer-card:hover .offer-source{text-decoration:underline}
+.offer-card.is-alert{border-color:var(--danger)}.offer-card.is-alert .offer-price{color:var(--danger)}
+.status-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;display:inline-block}
+.status-ok{background:var(--ok-dot)}.status-warn{background:var(--warm)}.status-bad{background:var(--danger)}.status-mut{background:var(--slate)}
+.market-link{font-weight:600;font-size:.92rem;color:var(--petrol-deep);text-decoration:none}
+.market-link:hover{text-decoration:underline}
+.empty-note{color:var(--slate);font-size:.85rem;padding:.6rem .2rem}