Fix: restliche CSP-blockierte Inline-Handler — Bild-Fallbacks (globaler data-fb Error-Handler) + Hover-Effekte (CSS-Utilities + data-hover-play)

App ist jetzt vollständig frei von Inline-Event-Handlern (onerror/onmouseenter/etc.).
data-fb Modi: hide/hide-parent/dim-grandparent/sibling/show-el/emoji/initials + data-fb-src.
Hover: .by-hover-lift/-surface2/-surface3 in utilities.css. SW v1165
This commit is contained in:
rene 2026-06-04 16:22:43 +02:00
parent 2ddd8ac350
commit c07b1cc01b
23 changed files with 125 additions and 68 deletions

View file

@ -1 +1 @@
1164
1165

View file

@ -63,3 +63,14 @@
font-weight: 600;
margin-bottom: var(--space-1);
}
/* ------------------------------------------------------------------
Hover-Utilities ersetzen CSP-blockierte onmouseenter/leave/over.
:hover braucht !important, da Inline-Base-Styles höher spezifisch sind.
------------------------------------------------------------------ */
.by-hover-lift { transition: transform .15s, box-shadow .15s; }
.by-hover-lift:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; }
.by-hover-surface2 { transition: background .15s; }
.by-hover-surface2:hover{ background: var(--c-surface-2) !important; }
.by-hover-surface3 { transition: background .15s; }
.by-hover-surface3:hover{ background: var(--c-surface-3) !important; }

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1164"></script>
<script src="/js/boot-early.js?v=1165"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1164">
<link rel="stylesheet" href="/css/layout.css?v=1164">
<link rel="stylesheet" href="/css/components.css?v=1164">
<link rel="stylesheet" href="/css/utilities.css?v=1164">
<link rel="stylesheet" href="/css/lists.css?v=1164">
<link rel="stylesheet" href="/css/design-system.css?v=1165">
<link rel="stylesheet" href="/css/layout.css?v=1165">
<link rel="stylesheet" href="/css/components.css?v=1165">
<link rel="stylesheet" href="/css/utilities.css?v=1165">
<link rel="stylesheet" href="/css/lists.css?v=1165">
</head>
<body>
@ -617,11 +617,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1164"></script>
<script src="/js/ui.js?v=1164"></script>
<script src="/js/app.js?v=1164"></script>
<script src="/js/worlds.js?v=1164"></script>
<script src="/js/offline-indicator.js?v=1164"></script>
<script src="/js/api.js?v=1165"></script>
<script src="/js/ui.js?v=1165"></script>
<script src="/js/app.js?v=1165"></script>
<script src="/js/worlds.js?v=1165"></script>
<script src="/js/offline-indicator.js?v=1165"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1164"></script>
<script src="/js/boot.js?v=1165"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1164'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1165'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
@ -434,6 +434,62 @@ const App = (() => {
// NAVIGATION EVENTS
// ----------------------------------------------------------
function _bindNavigation() {
// Globaler Bild-Fallback — ersetzt CSP-blockierte onerror-Attribute.
// 'error' bubbelt nicht → Capture-Phase. Greift nur bei [data-fb]/[data-fb-src].
document.addEventListener('error', e => {
const el = e.target;
if (!el || el.tagName !== 'IMG') return;
const fb = el.dataset.fb, altSrc = el.dataset.fbSrc;
if (fb === undefined && altSrc === undefined) return;
// Schritt 1: Alternative Quelle versuchen (z.B. _preview → Original / Platzhalter)
if (altSrc && !el.dataset.fbTried) {
el.dataset.fbTried = '1';
el.src = altSrc;
return;
}
// Schritt 2: terminaler Fallback
switch (fb) {
case 'hide-parent':
if (el.parentElement) el.parentElement.style.display = 'none';
break;
case 'dim-grandparent':
if (el.parentElement?.parentElement) el.parentElement.parentElement.style.opacity = '.4';
break;
case 'sibling':
el.style.display = 'none';
if (el.nextElementSibling) el.nextElementSibling.style.display = 'flex';
break;
case 'show-el': {
el.style.display = 'none';
const t = el.dataset.fbEl && document.getElementById(el.dataset.fbEl);
if (t) t.style.display = 'flex';
break;
}
case 'emoji':
if (el.parentElement) el.parentElement.innerHTML =
`<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:${el.dataset.fbSize || '2rem'}">${el.dataset.fbEmoji || '🐾'}</div>`;
break;
case 'initials': {
const sz = parseInt(el.dataset.fbSize, 10) || 40;
el.outerHTML =
`<div style="width:${sz}px;height:${sz}px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;font-size:${Math.round(sz * 0.45)}px;font-weight:700;color:var(--c-primary)">${el.dataset.fbInitials || ''}</div>`;
break;
}
default: // 'hide'
el.style.display = 'none';
el.classList.add('img-broken');
}
}, true);
// Video-Vorschau bei Hover (ersetzt CSP-blockierte onmouseenter/leave).
// <video> hat keine Kinder → e.target ist das Video selbst (matches() O(1)).
document.addEventListener('mouseover', e => {
if (e.target.matches?.('[data-hover-play]')) e.target.play?.().catch(() => {});
});
document.addEventListener('mouseout', e => {
if (e.target.matches?.('[data-hover-play]')) e.target.pause?.();
});
// Bottom Nav + Sidebar Klicks
document.addEventListener('click', e => {
const item = e.target.closest('[data-page]');

View file

@ -2628,9 +2628,7 @@ window.Page_admin = (() => {
</thead>
<tbody>
${log.map((l, i) => `
<tr data-log-idx="${i}" style="border-bottom:1px solid var(--c-border);cursor:pointer"
onmouseover="this.style.background='var(--c-surface-2)'"
onmouseout="this.style.background=''">
<tr data-log-idx="${i}" class="by-hover-surface2" style="border-bottom:1px solid var(--c-border);cursor:pointer">
<td class="p-2">${accountBadge(l.from_account)}</td>
<td class="p-2">${UI.escape(l.recipient)}</td>
<td style="padding:var(--space-2);color:var(--c-text-secondary)">${UI.escape(l.subject)}</td>

View file

@ -357,7 +357,7 @@ window.Page_adoption = (() => {
const foto = a.foto_url
? `<img src="${UI.escape(a.foto_url)}" alt="${UI.escape(a.name)}"
style="width:100%;height:100%;object-fit:cover"
onerror="this.parentElement.innerHTML='<div style=&quot;display:flex;align-items:center;justify-content:center;height:100%;font-size:2rem&quot;>🐶</div>'">`
data-fb="emoji" data-fb-emoji="🐶" data-fb-size="2rem">`
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐶</div>';
const distTxt = a.distanz_km != null ? `${a.distanz_km} km` : '';
@ -366,13 +366,10 @@ window.Page_adoption = (() => {
const tierheim = a.tierheim || '';
return `
<div data-adp-url="${UI.escape(a.adoptions_url)}"
<div data-adp-url="${UI.escape(a.adoptions_url)}" class="by-hover-lift"
style="border-radius:var(--radius-md);overflow:hidden;
background:var(--c-surface-2);cursor:pointer;
box-shadow:0 1px 4px rgba(0,0,0,0.08);
transition:transform .15s,box-shadow .15s"
onmouseenter="this.style.transform='translateY(-2px)';this.style.boxShadow='0 4px 12px rgba(0,0,0,0.12)'"
onmouseleave="this.style.transform='';this.style.boxShadow='0 1px 4px rgba(0,0,0,0.08)'">
box-shadow:0 1px 4px rgba(0,0,0,0.08)">
<div style="height:120px;overflow:hidden;background:var(--c-surface-3)">
${foto}
</div>
@ -459,14 +456,11 @@ window.Page_adoption = (() => {
function _shelterRow(s) {
return `
<a href="${UI.escape(s.url)}" target="_blank" rel="noopener noreferrer"
<a href="${UI.escape(s.url)}" target="_blank" rel="noopener noreferrer" class="by-hover-surface3"
style="display:flex;align-items:center;gap:var(--space-3);
padding:var(--space-3);border-radius:var(--radius-md);
background:var(--c-surface-2);text-decoration:none;color:inherit;
border:1px solid var(--c-border);
transition:background .15s"
onmouseenter="this.style.background='var(--c-surface-3)'"
onmouseleave="this.style.background='var(--c-surface-2)'">
border:1px solid var(--c-border)">
<div style="width:40px;height:40px;border-radius:50%;
background:var(--c-primary-light,#ede9fe);flex-shrink:0;
display:flex;align-items:center;justify-content:center;
@ -612,7 +606,7 @@ window.Page_adoption = (() => {
const foto = l.foto_url
? `<img src="${UI.escape(l.foto_url)}" alt="${UI.escape(l.name)}"
style="width:100%;height:100%;object-fit:cover"
onerror="this.parentElement.innerHTML='<div style=&quot;display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem&quot;>🐾</div>'">`
data-fb="emoji" data-fb-emoji="🐾" data-fb-size="2.5rem">`
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐾</div>';
const isActive = !l.status || l.status === 'active';

View file

@ -162,7 +162,7 @@ window.Page_breeder_editor = (() => {
<div style="position:relative;aspect-ratio:1;border-radius:var(--radius-md);overflow:hidden;background:var(--c-surface-2)">
${isVid
? `<video src="${UI.escape(ph.url)}" style="width:100%;height:100%;object-fit:cover" muted playsinline loop
onmouseenter="this.play()" onmouseleave="this.pause()"></video>
data-hover-play></video>
<div style="position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55);border-radius:4px;padding:1px 5px;font-size:10px;color:#fff"> Video</div>`
: `<img src="${UI.escape(ph.thumbnail_url || ph.url)}" style="width:100%;height:100%;object-fit:cover">`}
${ph.is_primary ? `<div style="position:absolute;top:4px;left:4px;background:rgba(196,132,58,.9);border-radius:3px;padding:1px 5px;font-size:9px;color:#fff;font-weight:700">LOGO</div>` : ''}

View file

@ -91,7 +91,7 @@ window.Page_breeder = (() => {
? `<img src="${UI.escape(p.logo_url)}" alt="Zwinger-Logo"
style="width:72px;height:72px;border-radius:50%;object-fit:cover;
border:3px solid rgba(255,255,255,.5);flex-shrink:0;box-shadow:0 2px 12px rgba(0,0,0,.25)"
onerror="this.style.display='none'">`
data-fb="hide">`
: `<div style="background:rgba(255,255,255,.15);border-radius:50%;width:64px;height:64px;
display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg style="width:32px;height:32px" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#paw-print"></use></svg>
@ -214,7 +214,7 @@ window.Page_breeder = (() => {
<img src="${UI.escape(ph.thumb)}" alt="${UI.escape(ph.caption)}"
loading="${i < 6 ? 'eager' : 'lazy'}"
style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'">
data-fb="hide-parent">
${ph.primary ? `<span style="position:absolute;top:4px;left:4px;background:var(--c-primary);
color:white;font-size:9px;font-weight:700;border-radius:999px;padding:1px 6px">Logo</span>` : ''}
${ph.caption ? `<div style="position:absolute;bottom:0;left:0;right:0;
@ -386,7 +386,7 @@ window.Page_breeder = (() => {
border:1px solid var(--c-border);aspect-ratio:1">
<img src="${UI.escape(ph.thumbnail_url||ph.url||'')}" alt="${UI.escape(ph.caption||'')}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'">
data-fb="hide-parent">
</a>`).join('')}
</div>
</div>`;

View file

@ -500,7 +500,7 @@ window.Page_diary = (() => {
const icon = L.divIcon({
html: hasPhoto
? `<div style="width:44px;height:44px;border-radius:50%;overflow:hidden;border:3px solid var(--c-primary,#C4843A);box-shadow:0 2px 8px rgba(0,0,0,.3);background:#fff">
<img src="${UI.escape(loc.cover_preview_url || loc.cover_url)}" style="width:100%;height:100%;object-fit:cover" onerror="this.src='${UI.escape(loc.cover_url)}'">
<img src="${UI.escape(loc.cover_preview_url || loc.cover_url)}" style="width:100%;height:100%;object-fit:cover" data-fb-src="${UI.escape(loc.cover_url)}">
</div>`
: `<div style="width:32px;height:32px;border-radius:50%;background:var(--c-primary,#C4843A);border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center">
<svg style="width:16px;height:16px;fill:#fff" viewBox="0 0 256 256"><path d="M128,16a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,16Zm0,176a80,80,0,1,1,80-80A80.09,80.09,0,0,1,128,192Zm0-104a24,24,0,1,0,24,24A24,24,0,0,0,128,88Z"/></svg>
@ -513,7 +513,7 @@ window.Page_diary = (() => {
const marker = L.marker([loc.gps_lat, loc.gps_lon], { icon });
marker.bindPopup(`
<div style="min-width:160px;cursor:pointer" class="diary-map-popup" data-id="${loc.id}">
${hasPhoto ? `<img src="${UI.escape(loc.cover_preview_url || loc.cover_url)}" style="width:100%;height:100px;object-fit:cover;border-radius:6px;display:block;margin-bottom:8px" onerror="this.src='${UI.escape(loc.cover_url)}'">` : ''}
${hasPhoto ? `<img src="${UI.escape(loc.cover_preview_url || loc.cover_url)}" style="width:100%;height:100px;object-fit:cover;border-radius:6px;display:block;margin-bottom:8px" data-fb-src="${UI.escape(loc.cover_url)}">` : ''}
<div style="font-weight:600;font-size:13px;margin-bottom:2px">${title}</div>
<div style="font-size:11px;color:#888">${dateStr}</div>
${loc.media_count > 1 ? `<div style="font-size:11px;color:#888;margin-top:2px">📷 ${loc.media_count} Medien</div>` : ''}
@ -569,7 +569,7 @@ window.Page_diary = (() => {
<img src="${UI.escape(m.preview_url || m.url)}"
${m.preview_url ? `srcset="${UI.escape(m.preview_url)} 800w, ${UI.escape(m.url)} 2000w" sizes="(max-width:400px) 200px, 400px"` : ''}
alt="" loading="lazy"
onerror="this.src='${UI.escape(m.url)}'">
data-fb-src="${UI.escape(m.url)}">
</div>`).join('')
}</div>`;
content.querySelectorAll('.diary-mosaic-item').forEach(el => {
@ -619,7 +619,7 @@ window.Page_diary = (() => {
const key = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const entry = byDate[key];
cells.push(`<div class="diary-cal-cell${entry?' has-entry':''}${key===today?' today':''}" data-entry-id="${entry?.id||''}">
${entry?.cover_url ? `<img src="${UI.escape(entry.cover_preview_url || entry.cover_url)}" alt="" loading="lazy" onerror="this.src='${UI.escape(entry.cover_url)}'">` : ''}
${entry?.cover_url ? `<img src="${UI.escape(entry.cover_preview_url || entry.cover_url)}" alt="" loading="lazy" data-fb-src="${UI.escape(entry.cover_url)}">` : ''}
<span class="diary-cal-day">${d}</span>
</div>`);
}
@ -812,7 +812,7 @@ window.Page_diary = (() => {
<img src="${e.cover_preview_url || e.cover_url || coverMedia.preview_url || coverMedia.url}"
${(e.cover_preview_url && e.cover_url) ? `srcset="${UI.escape(e.cover_preview_url)} 800w, ${UI.escape(e.cover_url)} 2000w" sizes="(max-width:600px) 300px, 600px"` : ''}
alt="Foto" loading="lazy"
${e.cover_url ? `onerror="this.src='${UI.escape(e.cover_url)}'"` : ''}>
${e.cover_url ? `data-fb-src="${UI.escape(e.cover_url)}"` : ''}>
${mediaCount > 1 ? `<span class="diary-card-media-count">${mediaCount}</span>` : ''}
</div>`;
}

View file

@ -448,7 +448,7 @@ function _fmtDate(iso) {
: `<img class="forum-card-thumb" src="${UI.escape(t.foto_preview_url || t.foto_preview)}"
${(t.foto_preview_url && t.foto_preview) ? `srcset="${UI.escape(t.foto_preview_url)} 800w" sizes="120px"` : ''}
alt="" loading="lazy"
onerror="this.src='${UI.escape(t.foto_preview)}'">`
data-fb-src="${UI.escape(t.foto_preview)}">`
: '';
return `

View file

@ -298,11 +298,11 @@ window.Page_friends = (() => {
const avatar = item.dog_foto
? `<img src="${UI.escape(item.dog_foto)}" alt="${UI.escape(item.dog_name || '')}"
loading="lazy" decoding="async" onerror="this.style.display='none'"
loading="lazy" decoding="async" data-fb="hide"
class="fr-activity-avatar">`
: item.avatar_url
? `<img src="${UI.escape(item.avatar_url)}" alt="${UI.escape(item.user_name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'"
loading="lazy" decoding="async" data-fb="hide"
class="fr-activity-avatar">`
: `<div class="fr-activity-avatar fr-activity-avatar--initial">
${UI.escape((item.user_name || '?')[0].toUpperCase())}
@ -556,7 +556,7 @@ window.Page_friends = (() => {
${withPhotos.slice(0, 4).map(d => `
<div class="text-center">
<img src="${UI.escape(d.foto_url)}" alt="${UI.escape(d.name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'"
loading="lazy" decoding="async" data-fb="hide"
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-surface)">
<div style="font-size:10px;color:var(--c-text-muted);margin-top:2px;
@ -580,7 +580,7 @@ window.Page_friends = (() => {
<div class="text-center">
${d.foto_url
? `<img src="${UI.escape(d.foto_url)}" alt="${UI.escape(d.name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'"
loading="lazy" decoding="async" data-fb="hide"
style="width:72px;height:72px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-primary);margin-bottom:var(--space-2)">`
: `<div style="width:72px;height:72px;border-radius:50%;
@ -803,13 +803,13 @@ window.Page_friends = (() => {
function _userAvatar(name, firstDog, avatarUrl) {
if (avatarUrl) {
return `<img src="${UI.escape(avatarUrl)}" alt="${UI.escape(name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'"
loading="lazy" decoding="async" data-fb="hide"
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-primary);flex-shrink:0">`;
}
if (firstDog?.foto_url) {
return `<img src="${UI.escape(firstDog.foto_url)}" alt="${UI.escape(firstDog.name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'"
loading="lazy" decoding="async" data-fb="hide"
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-primary);flex-shrink:0">`;
}

View file

@ -39,7 +39,7 @@ window.Page_laeufi = (() => {
? `<img src="${UI.escape(logoUrl)}" alt="Logo"
style="width:48px;height:48px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,.5);flex-shrink:0"
onerror="this.style.display='none'">`
data-fb="hide">`
: `<div style="width:48px;height:48px;border-radius:50%;background:rgba(196,132,58,.15);
border:2px solid rgba(196,132,58,.4);display:flex;align-items:center;
justify-content:center;flex-shrink:0">

View file

@ -100,7 +100,7 @@ window.Page_litters = (() => {
? `<img src="${UI.escape(logoUrl)}" alt="Logo"
style="width:48px;height:48px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,.5);flex-shrink:0"
onerror="this.style.display='none'">`
data-fb="hide">`
: `<div style="width:48px;height:48px;border-radius:50%;background:rgba(196,132,58,.15);
border:2px solid rgba(196,132,58,.4);display:flex;align-items:center;
justify-content:center;flex-shrink:0">
@ -1358,7 +1358,7 @@ window.Page_litters = (() => {
<img src="${UI.escape(thumb)}" alt="${UI.escape(ph.caption || '')}"
loading="lazy"
style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.src='/static/img/placeholder.webp'">
data-fb-src="/static/img/placeholder.webp">
</a>
<button class="photos-vis-btn"
data-photo-id="${ph.id}"

View file

@ -114,9 +114,9 @@ window.Page_moderation = (() => {
}
function _statCard(icon, label, value, color, tab) {
const clickable = tab ? `data-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer;transition:box-shadow .15s,transform .15s" onmouseenter="this.style.boxShadow='var(--shadow-md)';this.style.transform='translateY(-2px)'" onmouseleave="this.style.boxShadow='';this.style.transform=''"` : `style="padding:var(--space-4);text-align:center"`;
const clickable = tab ? `data-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer"` : `style="padding:var(--space-4);text-align:center"`;
return `
<div class="card mod-stat-card" ${clickable}>
<div class="card mod-stat-card${tab ? ' by-hover-lift' : ''}" ${clickable}>
<svg class="ph-icon" style="width:24px;height:24px;color:${color};
margin-bottom:var(--space-2)" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>

View file

@ -155,7 +155,7 @@ window.Page_partner_profil = (() => {
background:var(--c-surface-2)">
${isVid
? `<video src="${UI.escape(url)}" style="width:100%;height:100%;object-fit:cover" muted playsinline loop
onmouseenter="this.play()" onmouseleave="this.pause()"></video>
data-hover-play></video>
<div style="position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55);
border-radius:4px;padding:1px 5px;font-size:10px;color:#fff"> Video</div>`
: `<img src="${UI.escape(url)}" style="width:100%;height:100%;object-fit:cover">`}

View file

@ -26,7 +26,7 @@ function _fmtDate(iso) {
if (foto_url) {
return `<img src="${UI.escape(foto_url)}" alt="${initials}"
style="width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;display:block;"
onerror="this.outerHTML='<div style=\'width:${size}px;height:${size}px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.45)}px;font-weight:700;color:var(--c-primary);\'>${initials}</div>'">`;
data-fb="initials" data-fb-initials="${initials}" data-fb-size="${size}">`;
}
return `<div style="width:${size}px;height:${size}px;border-radius:50%;
background:var(--c-primary-subtle);display:flex;align-items:center;

View file

@ -3101,11 +3101,9 @@ window.Page_routes = (() => {
const friendRows = friends.map(f => {
const initial = (f.name || '?')[0].toUpperCase();
return `<div class="rk-friend-row" data-id="${f.id}" data-name="${UI.escape(f.name || 'Anonym')}"
return `<div class="rk-friend-row by-hover-surface2" data-id="${f.id}" data-name="${UI.escape(f.name || 'Anonym')}"
style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);
cursor:pointer;border-radius:var(--radius-md);transition:background .15s"
onmouseover="this.style.background='var(--c-surface-2)'"
onmouseout="this.style.background=''">
cursor:pointer;border-radius:var(--radius-md)">
<div style="width:36px;height:36px;border-radius:50%;background:var(--c-primary);
color:#fff;display:flex;align-items:center;justify-content:center;
font-weight:600;flex-shrink:0">${UI.escape(initial)}</div>

View file

@ -658,7 +658,7 @@ window.Page_social = (() => {
box-shadow:var(--shadow-xs)">
${mediaUrl ? `<img src="${mediaUrl}"
style="width:60px;height:60px;border-radius:var(--radius-md);object-fit:cover;flex-shrink:0"
onerror="this.style.display='none'">` : '<span style="font-size:2.2em">🐶</span>'}
data-fb="hide">` : '<span style="font-size:2.2em">🐶</span>'}
<div>
<div style="font-size:11px;color:var(--c-primary);font-weight:600;margin-bottom:2px">
Rasse des Tages</div>
@ -882,7 +882,7 @@ window.Page_social = (() => {
<div class="sm-label">📎 Dein Medien-Upload</div>
<img src="${mediaUrl}" style="max-width:100%;max-height:200px;
border-radius:var(--radius-md);object-fit:cover;margin-top:8px"
onerror="this.style.display='none'">
data-fb="hide">
</div>` : ''}
${_resultBlock('📝 Caption', data.caption, true)}

View file

@ -1106,7 +1106,7 @@ window.Page_walks = (() => {
return `
<div class="challenge-sub-card">
<img src="${UI.escape(s.foto_url)}" alt="Challenge-Foto" loading="lazy"
onerror="this.src='/icons/icon-192.png'"
data-fb-src="/icons/icon-192.png"
data-lightbox-url="${UI.escape(s.foto_url)}">
<div class="challenge-sub-info">
<div class="challenge-sub-user">${UI.icon('user')} ${UI.escape(s.user_name || 'Anonym')}
@ -1130,7 +1130,7 @@ window.Page_walks = (() => {
winners.map(w => {
if (!w.winner) return `<div class="challenge-winner-chip"><span>${UI.escape(w.challenge.thema)}</span><small>Kein Gewinner</small></div>`;
return `<div class="challenge-winner-chip">
<img src="${UI.escape(w.winner.foto_url)}" alt="Gewinner" onerror="this.src='/icons/icon-192.png'">
<img src="${UI.escape(w.winner.foto_url)}" alt="Gewinner" data-fb-src="/icons/icon-192.png">
<div>
<div style="font-weight:600;font-size:var(--text-xs)">${UI.escape(w.challenge.thema)}</div>
<div class="text-xs-secondary">${UI.escape(w.winner.user_name)} · ${w.winner.votes} </div>

View file

@ -403,7 +403,7 @@ window.Page_wiki = (() => {
: fotoUrl;
const photoHtml = fotoUrl
? `<img class="wiki-breed-photo" src="${UI.escape(srcUrl)}" loading="lazy" alt="${UI.escape(r.name)}"
onerror="if(this.src.includes('_preview')){this.src='${UI.escape(fotoUrl)}'}else{this.style.display='none';this.nextElementSibling.style.display='flex'}">`
data-fb="sibling" data-fb-src="${UI.escape(fotoUrl)}">`
: '';
const fallbackHtml = `<div class="wiki-breed-photo-fallback" style="${fotoUrl ? 'display:none' : ''}">${_DOG_SILHOUETTE}</div>`;
@ -757,7 +757,7 @@ window.Page_wiki = (() => {
? `<div class="wiki-gallery-wrap">
<img class="wiki-detail-photo wiki-gallery-main" id="wiki-main-photo"
src="${UI.escape(allFotos[0].foto_url)}" alt="${UI.escape(rasse.name)}"
onerror="this.style.display='none';document.getElementById('wiki-photo-fallback').style.display='flex'">
data-fb="show-el" data-fb-el="wiki-photo-fallback">
<div id="wiki-photo-fallback" class="wiki-detail-photo-placeholder hidden">${_dogSvgLg}<span>Kein Foto verfügbar</span></div>
${allFotos.length > 1 ? `
<div class="wiki-gallery-strip" id="wiki-gallery-strip">
@ -766,7 +766,7 @@ window.Page_wiki = (() => {
aria-label="Foto ${i + 1}">
<img src="${UI.escape(f.foto_url.startsWith('/media/') ? f.foto_url.replace(/\.(jpe?g|png|gif|webp)$/i,'_preview.webp') : f.foto_url)}"
alt="" loading="lazy"
onerror="if(this.src.includes('_preview')){this.src='${UI.escape(f.foto_url)}'}else{this.style.display='none'}">
data-fb-src="${UI.escape(f.foto_url)}">
${f.user_name ? `<span class="wiki-gallery-thumb-label">von ${UI.escape(f.user_name)}</span>` : ''}
</button>`).join('')}
</div>` : ''}
@ -1238,7 +1238,7 @@ window.Page_wiki = (() => {
const cardsHtml = data.results.map(r => {
const photoHtml = r.foto_url
? `<img class="wiki-quiz-result-photo" src="${UI.escape(r.foto_url)}" loading="lazy" alt="${UI.escape(r.name)}" onerror="this.style.display='none'">`
? `<img class="wiki-quiz-result-photo" src="${UI.escape(r.foto_url)}" loading="lazy" alt="${UI.escape(r.name)}" data-fb="hide">`
: `<div class="wiki-quiz-result-photo-fallback">${UI.icon('dog')}</div>`;
return `
<div class="wiki-quiz-result-card">

View file

@ -102,7 +102,7 @@ window.Page_zuchthunde = (() => {
? `<img src="${UI.escape(logoUrl)}" alt="Logo"
style="width:48px;height:48px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,.5);flex-shrink:0"
onerror="this.style.display='none'">`
data-fb="hide">`
: `<div style="width:48px;height:48px;border-radius:50%;background:rgba(196,132,58,.15);
border:2px solid rgba(196,132,58,.4);display:flex;align-items:center;
justify-content:center;flex-shrink:0">
@ -1751,7 +1751,7 @@ window.Page_zuchthunde = (() => {
<a href="${UI.escape(ph.url || '')}" target="_blank" rel="noopener noreferrer">
<img src="${UI.escape(thumb)}" alt="${UI.escape(ph.caption || '')}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.parentElement.style.opacity='.4'">
data-fb="dim-grandparent">
</a>
${isPrimary ? `<span style="position:absolute;top:3px;left:3px;background:var(--c-primary);color:white;
font-size:9px;font-weight:700;border-radius:999px;padding:1px 5px">Logo</span>` : ''}

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1164"></script>
<script src="/js/landing-init.js?v=1165"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1164';
const VER = '1165';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten