diff --git a/public/app.js b/public/app.js
index 6c90a48..d11bb70 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,4 +1,4 @@
-const C24 = u => u; // check24 proxy urls already absolute
+const IMG = u => '/img?u=' + encodeURIComponent(u); // route images through same-origin proxy
const REG = {
kroatien:{flag:'ðŸ‡ðŸ‡·',name:'Kroatien',color:'var(--kro)',mk:'#e8643c'},
kanaren:{flag:'🇮🇨',name:'Kanaren',color:'var(--kan)',mk:'#f1a23b'},
@@ -119,7 +119,7 @@ let STATE = {votes:{}};
/* ---------- hero ---------- */
document.getElementById('herobg').style.backgroundImage =
- "url('"+byId('bluesun').imgs[0]+"')";
+ "url('"+IMG(byId('bluesun').imgs[0])+"')";
/* ---------- voter selector ---------- */
const whoEl = document.getElementById('who');
@@ -166,7 +166,7 @@ function avgFor(id){
return {avg:n?sum/n:0,n,per};
}
function cardHTML(o){
- const imgs=o.imgs.map((s,i)=>`
`).join('');
+ const imgs=o.imgs.map((s,i)=>`
`).join('');
const dots=o.imgs.length>1?`
${o.imgs.map((_,i)=>``).join('')}
`:'';
const nav=o.imgs.length>1?'':'';
const rate=o.rate?`${o.rate} ${o.rlabel}${o.bew?(' · '+o.bew+' Bew.'):''}`:(o.bew?`${o.bew} Bewertungen`:'');
@@ -256,10 +256,11 @@ async function sendVote(option,stars){
try{const r=await fetch('/api/vote',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({voter:me_,option,stars})});STATE=await r.json();renderVotes();}catch(e){}
}
-function flashSaved(){const s=document.getElementById('saved');s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1400);}
+function flashSaved(msg){const s=document.getElementById('saved');s.textContent=msg||'✓ gespeichert';s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1800);}
document.getElementById('resetBtn').onclick=async()=>{
if(!confirm('Wirklich ALLE Bewertungen von Till, Lea und Astrid zurücksetzen?'))return;
- try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved();}catch(e){}
+ try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved('✓ Alle Bewertungen gelöscht');}
+ catch(e){alert('Zurücksetzen fehlgeschlagen – bitte nochmal versuchen.');}
};
function renderAll(){renderWho();renderRecos();renderRegions();}
diff --git a/server.js b/server.js
index bb1f97c..6d4c7e6 100644
--- a/server.js
+++ b/server.js
@@ -30,6 +30,12 @@ function saveState(state) {
}
let state = loadState();
+// In-memory image cache for the proxy
+const imgCache = new Map();
+function hostAllowed(host) {
+ return /(^|\.)urlaub\.check24\.de$/.test(host) || host === 'files.ahoi-schiff.de';
+}
+
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml',
'.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json' };
@@ -72,6 +78,34 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/health') { return sendJSON(res, 200, { ok: true }); }
+ // ---- Image proxy (defeats hotlink/referrer protection, same-origin = always loads) ----
+ if (p === '/img' && req.method === 'GET') {
+ const u = url.searchParams.get('u');
+ let host;
+ try { host = new URL(u).host; } catch (e) { res.writeHead(400); return res.end('bad url'); }
+ if (!hostAllowed(host)) { res.writeHead(403); return res.end('host not allowed'); }
+ if (imgCache.has(u)) {
+ const c = imgCache.get(u);
+ res.writeHead(200, { 'Content-Type': c.type, 'Cache-Control': 'public, max-age=604800' });
+ return res.end(c.buf);
+ }
+ try {
+ const referer = host.endsWith('ahoi-schiff.de') ? 'https://www.ahoi-schiff.de/' : 'https://urlaub.check24.de/';
+ const ac = new AbortController();
+ const t = setTimeout(() => ac.abort(), 8000);
+ const r = await fetch(u, { signal: ac.signal, headers: {
+ 'Referer': referer, 'User-Agent': 'Mozilla/5.0 (compatible; HeidrichReise/1.0)', 'Accept': 'image/*'
+ }});
+ clearTimeout(t);
+ if (!r.ok) { res.writeHead(502); return res.end('upstream ' + r.status); }
+ const type = r.headers.get('content-type') || 'image/jpeg';
+ const buf = Buffer.from(await r.arrayBuffer());
+ if (imgCache.size < 300) imgCache.set(u, { buf, type });
+ res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'public, max-age=604800' });
+ return res.end(buf);
+ } catch (e) { res.writeHead(502); return res.end('proxy error'); }
+ }
+
// ---- Static ----
let file = p === '/' ? '/index.html' : decodeURIComponent(p);
const full = path.join(PUBLIC, path.normalize(file).replace(/^(\.\.[\/\\])+/, ''));