Refactor: 1167 _esc() → UI.escape() in 36 Dateien, SW by-v1113

Bündel 1 aus dem Duplikat-Audit: existierende zentrale Helper nutzen
statt lokale Duplikate.

Pure Migration ohne neuen Code:
- 1167 _esc()-Aufrufe in 36 Page-Modulen migriert auf UI.escape()
- 24 lokale _esc/_escape-Definitionen entfernt
- lost.js hatte _escape() (Variante) — 17 Aufrufe ebenfalls migriert
- jobs.js + breeder.js: tote Alias-Wrapper entfernt

UI.escape() existierte schon — wurde nur überall lokal nochmal
implementiert. Funktional identisch (gleiche 4-replace-chain für
& < > ").

Tests 19/19 grün. Frontend-LOC um ~120 Zeilen reduziert.

Hinweis: _emptyState (7 Stellen) und _icon (8 Stellen) wurden NICHT
migriert — sie haben abweichende Signaturen von UI.emptyState({...})
bzw. UI.icon(name). Eigener Sprint nötig.
This commit is contained in:
rene 2026-05-27 10:15:33 +02:00
parent e7939ce98e
commit c517c9281d
42 changed files with 1115 additions and 1341 deletions

View file

@ -202,7 +202,7 @@ window.Page_admin = (() => {
// Manager-Tabelle
const managerRows = d.managers.map(m => `
<tr>
<td style="font-weight:600">${_esc(m.name)}</td>
<td style="font-weight:600">${UI.escape(m.name)}</td>
<td class="text-right">${m.published}</td>
<td class="text-right">${m.with_link}
${m.published > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)">
@ -241,13 +241,13 @@ window.Page_admin = (() => {
<tr>
<td style="color:var(--c-text-muted);white-space:nowrap">${_fmt(p.published_at)}</td>
<td style="font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis;
white-space:nowrap">${_esc(p.topic)}</td>
white-space:nowrap">${UI.escape(p.topic)}</td>
<td>${_PL[p.platform]||p.platform||''}</td>
<td style="font-size:10px;color:var(--c-text-muted)">${_esc(p.category||'')}</td>
<td style="font-size:10px;color:var(--c-text-muted)">${UI.escape(p.category||'')}</td>
<td>${p.ai_score ? '⭐'.repeat(p.ai_score) : ''}</td>
<td style="font-weight:500">${_esc(p.manager||'')}</td>
<td style="font-weight:500">${UI.escape(p.manager||'')}</td>
<td>${p.post_url
? `<a href="${_esc(p.post_url)}" target="_blank" rel="noopener"
? `<a href="${UI.escape(p.post_url)}" target="_blank" rel="noopener"
style="font-size:11px;color:var(--c-primary)">🔗 Link</a>`
: `<span style="font-size:11px;color:var(--c-text-muted)"></span>`}</td>
</tr>`).join('');
@ -319,7 +319,7 @@ window.Page_admin = (() => {
try { d = await API.get('/admin/analytics'); }
catch (err) {
el.innerHTML = `<div style="padding:var(--space-4);color:var(--c-danger);font-size:var(--text-sm)">
${UI.icon('warning')} Fehler: ${_esc(err.message || String(err))}</div>`;
${UI.icon('warning')} Fehler: ${UI.escape(err.message || String(err))}</div>`;
return;
}
@ -396,7 +396,7 @@ window.Page_admin = (() => {
const pct = ((p[valKey] ?? 0) / maxV * 100).toFixed(0);
return `<div>
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs);margin-bottom:3px">
<span style="color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:78%">${_esc(p[labelKey] || '—')}</span>
<span style="color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:78%">${UI.escape(p[labelKey] || '—')}</span>
<span style="color:var(--c-text-secondary);flex-shrink:0;margin-left:var(--space-2)">${fmt(p[valKey] ?? 0)}</span>
</div>
<div style="height:4px;border-radius:2px;background:var(--c-surface-2)">
@ -549,7 +549,7 @@ window.Page_admin = (() => {
box-shadow:0 0 4px ${dot}"></span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary);font-weight:600">${label}</span>
<span class="text-xs-muted">·</span>
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-family:monospace">${_esc(model)}</span>
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-family:monospace">${UI.escape(model)}</span>
<span style="margin-left:auto;font-size:10px;padding:1px 6px;border-radius:10px;
background:var(--c-surface);color:var(--c-text-muted);border:1px solid var(--c-border)">
Modus: ${ki.local_reachable ? 'local' : 'cloud'}
@ -641,8 +641,8 @@ window.Page_admin = (() => {
</tr></thead>
<tbody>
${(kiH.top_users).map(u => `<tr>
<td style="font-weight:600">${_esc(u.name)}</td>
<td class="text-muted">${_esc(u.email.length > 22 ? u.email.split('@')[1] : u.email)}</td>
<td style="font-weight:600">${UI.escape(u.name)}</td>
<td class="text-muted">${UI.escape(u.email.length > 22 ? u.email.split('@')[1] : u.email)}</td>
<td style="color:var(--c-primary);font-weight:600">${u.cloud}</td>
<td>${u.total}</td>
<td class="text-muted">${u.last_date?.slice(5) || '—'}</td>
@ -837,24 +837,24 @@ window.Page_admin = (() => {
background:var(--c-surface-2);
display:flex;align-items:center;justify-content:center;
font-weight:var(--weight-bold);color:var(--c-text-secondary)">
${_esc(u.name[0].toUpperCase())}
${UI.escape(u.name[0].toUpperCase())}
</div>
<!-- Info -->
<div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
${_esc(u.name)}
${UI.escape(u.name)}
${u.is_banned ? `<span style="font-size:10px;padding:1px 5px;border-radius:3px;
background:var(--c-danger);color:#fff;margin-left:4px">
GESPERRT</span>` : ''}
</div>
<div class="text-xs-muted">
${_esc(u.email)} ·
${UI.escape(u.email)} ·
<span style="color:${u.rolle === 'admin' ? 'var(--c-danger)' : u.rolle === 'moderator' ? '#f59e0b' : 'var(--c-text-muted)'}">
${_esc(u.rolle)}
${UI.escape(u.rolle)}
</span>
· <span style="color:${u.subscription_tier && u.subscription_tier !== 'standard' ? 'var(--c-primary)' : 'var(--c-text-muted)'}">
${_esc(u.subscription_tier || 'standard')}
${UI.escape(u.subscription_tier || 'standard')}
</span>
· ${u.dog_count} Hund${u.dog_count !== 1 ? 'e' : ''}
· ${u.thread_count} Threads
@ -875,28 +875,28 @@ window.Page_admin = (() => {
<!-- Aktionen -->
<div style="display:flex;gap:var(--space-1);flex-shrink:0">
${u.is_banned
? `<button class="btn btn-sm btn-ghost adm-unban" data-uid="${u.id}" data-name="${_esc(u.name)}"
? `<button class="btn btn-sm btn-ghost adm-unban" data-uid="${u.id}" data-name="${UI.escape(u.name)}"
title="Sperre aufheben" class="text-success">
<svg class="ph-icon"><use href="/icons/phosphor.svg#lock-open"></use></svg>
</button>`
: `<button class="btn btn-sm btn-ghost adm-ban" data-uid="${u.id}" data-name="${_esc(u.name)}"
: `<button class="btn btn-sm btn-ghost adm-ban" data-uid="${u.id}" data-name="${UI.escape(u.name)}"
title="Sperren" class="text-danger">
<svg class="ph-icon"><use href="/icons/phosphor.svg#lock"></use></svg>
</button>`
}
${isAdmin ? `
<button class="btn btn-sm btn-ghost adm-rolle" data-uid="${u.id}"
data-name="${_esc(u.name)}" data-rolle="${_esc(u.rolle)}"
data-name="${UI.escape(u.name)}" data-rolle="${UI.escape(u.rolle)}"
title="Rolle ändern">
<svg class="ph-icon"><use href="/icons/phosphor.svg#shield"></use></svg>
</button>
<button class="btn btn-sm btn-ghost adm-tier" data-uid="${u.id}"
data-name="${_esc(u.name)}" data-tier="${_esc(u.subscription_tier || 'standard')}"
data-name="${UI.escape(u.name)}" data-tier="${UI.escape(u.subscription_tier || 'standard')}"
title="Abo-Stufe ändern">
<svg class="ph-icon"><use href="/icons/phosphor.svg#star"></use></svg>
</button>
<button class="btn btn-sm btn-ghost adm-delete" data-uid="${u.id}"
data-name="${_esc(u.name)}" title="Löschen"
data-name="${UI.escape(u.name)}" title="Löschen"
class="text-danger">
<svg class="ph-icon"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
@ -1087,18 +1087,18 @@ window.Page_admin = (() => {
<div class="flex-1-min">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1)">
${r.resolved ? '✓ Erledigt · ' : ''}
${_esc(r.target_type)} #${r.target_id} ·
Gemeldet von <strong>${_esc(r.melder_name)}</strong>
${UI.escape(r.target_type)} #${r.target_id} ·
Gemeldet von <strong>${UI.escape(r.melder_name)}</strong>
</div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-1)">
Grund: ${_esc(r.grund)}
Grund: ${UI.escape(r.grund)}
</div>
${r.content_preview ? `
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-sm)">
${_esc(r.content_preview)}
${UI.escape(r.content_preview)}
</div>` : ''}
</div>
<div style="display:flex;flex-direction:column;gap:var(--space-2);flex-shrink:0">
@ -1175,10 +1175,10 @@ window.Page_admin = (() => {
<div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
${t.is_deleted ? '<s>' : ''}${_esc(t.titel)}${t.is_deleted ? '</s>' : ''}
${t.is_deleted ? '<s>' : ''}${UI.escape(t.titel)}${t.is_deleted ? '</s>' : ''}
</div>
<div class="text-xs-muted">
von ${_esc(t.autor_name)} ·
von ${UI.escape(t.autor_name)} ·
${t.antworten} Antworten ·
${t.is_pinned ? '📌 ' : ''}${t.is_locked ? '🔒 ' : ''}${t.is_deleted ? '🗑 gelöscht' : ''}
</div>
@ -1313,8 +1313,8 @@ window.Page_admin = (() => {
return `<div style="border-bottom:1px solid var(--c-border);padding:2px 0">` +
`<span class="text-muted">${r.t}</span> ` +
`<span style="color:${color};font-weight:600">${r.l}</span> ` +
`<span class="text-secondary">${_esc(r.n)}</span> ` +
`<span>${_esc(r.m)}</span></div>`;
`<span class="text-secondary">${UI.escape(r.n)}</span> ` +
`<span>${UI.escape(r.m)}</span></div>`;
}).join('') || '<span class="text-muted">Keine Einträge</span>';
};
el.querySelector('#adm-sys-refresh').addEventListener('click', () => {
@ -1385,13 +1385,13 @@ window.Page_admin = (() => {
const scoreBar = v => `<span style="color:${scoreColor(v)};font-weight:600">${v.toFixed(1)}</span>`;
const rows = d.results.filter(r => !r.error).map(r =>
`<tr>
<td style="padding:2px 6px">${_esc(r.name)}</td>
<td style="padding:2px 6px">${UI.escape(r.name)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.vollstaendigkeit)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.korrektheit)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.sprachqualitaet)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.konsistenz)}</td>
<td style="text-align:center;padding:2px 6px;font-weight:700">${scoreBar(r.gesamt)}</td>
<td style="padding:2px 6px;color:var(--c-text-muted);font-size:0.9em">${_esc(r.hinweis || '')}</td>
<td style="padding:2px 6px;color:var(--c-text-muted);font-size:0.9em">${UI.escape(r.hinweis || '')}</td>
</tr>`
).join('');
box.style.display = 'block';
@ -1459,9 +1459,9 @@ window.Page_admin = (() => {
</div>
</div>
<div style="margin-top:var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:4px">
<span>Python ${_esc(s.python_version)}</span>
<span>Python ${UI.escape(s.python_version)}</span>
<span style="font-family:monospace;font-size:var(--text-xs);background:var(--c-surface-2);padding:2px 8px;border-radius:4px;color:var(--c-text-secondary)">
APP v${typeof APP_VER !== 'undefined' ? APP_VER : '—'} · ${_esc(s.sw_version || '?')}
APP v${typeof APP_VER !== 'undefined' ? APP_VER : '—'} · ${UI.escape(s.sw_version || '?')}
</span>
</div>
</div>
@ -1504,9 +1504,9 @@ window.Page_admin = (() => {
const userRows = topUsers.map(u => {
const emailDisplay = (u.email || '').length > 20
? '@' + (u.email || '').split('@')[1]
: _esc(u.email || '');
: UI.escape(u.email || '');
return `<tr>
<td style="padding:5px 8px;font-weight:500">${_esc(u.name || '')}</td>
<td style="padding:5px 8px;font-weight:500">${UI.escape(u.name || '')}</td>
<td style="padding:5px 8px;color:var(--c-text-muted);font-size:var(--text-xs)">${emailDisplay}</td>
<td style="padding:5px 8px;text-align:right;font-weight:600">${u.total ?? 0}</td>
<td style="padding:5px 8px;text-align:right;color:var(--c-text-muted);font-size:var(--text-xs)">${u.last_week || ''}</td>
@ -1549,7 +1549,7 @@ window.Page_admin = (() => {
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs);
color:var(--c-text-muted);margin-bottom:var(--space-4)">
<span>30 Tage</span>
<span>${_esc(lastDate)}</span>
<span>${UI.escape(lastDate)}</span>
</div>
${topUsers.length ? `
@ -1704,12 +1704,12 @@ window.Page_admin = (() => {
</tr></thead><tbody>
${zuchterPending.map((z, i) => `
<tr style="${i%2===1?'background:var(--c-surface-2)':''}">
<td class="adm-td" style="font-weight:var(--weight-semibold)">${_esc(z.rasse_slug)}</td>
<td class="adm-td">${_esc(z.name)}${z.zwingername ? `<br><span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(z.zwingername)}</span>` : ''}</td>
<td class="adm-td">${_esc([z.plz, z.ort, z.bundesland].filter(Boolean).join(' '))}</td>
<td class="adm-td" style="font-weight:var(--weight-semibold)">${UI.escape(z.rasse_slug)}</td>
<td class="adm-td">${UI.escape(z.name)}${z.zwingername ? `<br><span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(z.zwingername)}</span>` : ''}</td>
<td class="adm-td">${UI.escape([z.plz, z.ort, z.bundesland].filter(Boolean).join(' '))}</td>
<td class="adm-td">${z.vdh_mitglied ? `<span style="color:var(--c-success);display:flex;align-items:center;gap:2px"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> VDH</span>` : '—'}</td>
<td class="adm-td">${_ageLabel(z.created_at)}</td>
<td class="adm-td">${z.website ? `<a href="${_esc(z.website)}" target="_blank" style="color:var(--c-primary);font-size:var(--text-xs)">Link</a>` : '—'}</td>
<td class="adm-td">${z.website ? `<a href="${UI.escape(z.website)}" target="_blank" style="color:var(--c-primary);font-size:var(--text-xs)">Link</a>` : '—'}</td>
<td class="adm-td" style="text-align:right;white-space:nowrap">
<button class="btn btn-sm btn-primary adm-zuchter-approve" data-id="${z.id}" style="margin-right:4px"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> Freigeben</button>
<button class="btn btn-sm btn-ghost adm-zuchter-delete" data-id="${z.id}" class="text-danger"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg></button>
@ -1719,8 +1719,8 @@ window.Page_admin = (() => {
}
// Züchter-History
if (zuchterDone.length) html += _historySection('Züchter-Einreichungen', zuchterDone,
z => `<span style="font-weight:600">${_esc(z.name)}</span> · ${_esc(z.rasse_slug)} ·
${UI.icon('check-circle')} ${_esc(z.verified_by_name||'?')} · ${(z.verified_at||'').slice(0,10)}`);
z => `<span style="font-weight:600">${UI.escape(z.name)}</span> · ${UI.escape(z.rasse_slug)} ·
${UI.icon('check-circle')} ${UI.escape(z.verified_by_name||'?')} · ${(z.verified_at||'').slice(0,10)}`);
// --- Wiki-Foto-Einreichungen ---
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1737,12 +1737,12 @@ window.Page_admin = (() => {
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-4);margin-bottom:var(--space-3)">
${fotosPending.map(f => `
<div class="card p-4">
<img src="${_esc(f.foto_url)}" alt=""
<img src="${UI.escape(f.foto_url)}" alt=""
style="width:100%;height:140px;object-fit:cover;border-radius:var(--radius-md);margin-bottom:var(--space-3)">
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${_esc(f.rasse_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">von ${_esc(f.user_name)}</div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${UI.escape(f.rasse_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">von ${UI.escape(f.user_name)}</div>
<div class="mb-3">${_ageLabel(f.created_at)}</div>
${f.aktuell_foto ? `<img src="${_esc(f.aktuell_foto)}" alt="Aktuell"
${f.aktuell_foto ? `<img src="${UI.escape(f.aktuell_foto)}" alt="Aktuell"
style="width:100%;height:80px;object-fit:cover;border-radius:var(--radius-sm);
opacity:.5;margin-bottom:var(--space-2)">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-3)"> aktuelles Foto</div>` : ''}
@ -1756,10 +1756,10 @@ window.Page_admin = (() => {
// Fotos-History
if (fotosDone.length) html += _historySection('Foto-Einreichungen', fotosDone,
f => `<img src="${_esc(f.foto_url)}" style="width:32px;height:32px;object-fit:cover;border-radius:4px;vertical-align:middle;margin-right:6px">
<span style="font-weight:600">${_esc(f.rasse_name||'?')}</span> · von ${_esc(f.user_name||'?')} ·
f => `<img src="${UI.escape(f.foto_url)}" style="width:32px;height:32px;object-fit:cover;border-radius:4px;vertical-align:middle;margin-right:6px">
<span style="font-weight:600">${UI.escape(f.rasse_name||'?')}</span> · von ${UI.escape(f.user_name||'?')} ·
${f.status==='approved' ? `${UI.icon('check-circle')} genehmigt` : `${UI.icon('x-circle')} abgelehnt`}
${f.reviewed_by_name ? ` von ${_esc(f.reviewed_by_name)}` : ''} · ${(f.reviewed_at||'').slice(0,10)}`);
${f.reviewed_by_name ? ` von ${UI.escape(f.reviewed_by_name)}` : ''} · ${(f.reviewed_at||'').slice(0,10)}`);
// --- Forum-Meldungen ---
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1780,16 +1780,16 @@ window.Page_admin = (() => {
<div style="display:flex;align-items:flex-start;gap:var(--space-3)">
<div class="flex-1-min">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1);display:flex;align-items:center;flex-wrap:wrap;gap:4px">
${_esc(r.target_type)} #${r.target_id} · Gemeldet von <strong>${_esc(r.melder_name || '?')}</strong>
${UI.escape(r.target_type)} #${r.target_id} · Gemeldet von <strong>${UI.escape(r.melder_name || '?')}</strong>
${_ageLabel(r.created_at)}
</div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);margin-bottom:var(--space-1)">
Grund: ${_esc(r.grund)}
Grund: ${UI.escape(r.grund)}
</div>
${r.content_preview ? `
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-sm)">${_esc(r.content_preview)}</div>` : ''}
border-radius:var(--radius-sm)">${UI.escape(r.content_preview)}</div>` : ''}
</div>
<button class="btn btn-sm btn-primary adm-mod-resolve" data-rid="${r.id}" title="Als erledigt markieren">
${UI.icon('check')}
@ -1801,8 +1801,8 @@ window.Page_admin = (() => {
// Meldungen-History
if (reportsDone.length) html += _historySection('Forum-Meldungen', reportsDone,
r => `${_esc(r.target_type)} #${r.target_id} · ${_esc(r.grund)} · Gemeldet von ${_esc(r.melder_name||'?')} ·
${UI.icon('check-circle')} ${_esc(r.resolved_by_name||'?')} · ${(r.resolved_at||'').slice(0,10)}`);
r => `${UI.escape(r.target_type)} #${r.target_id} · ${UI.escape(r.grund)} · Gemeldet von ${UI.escape(r.melder_name||'?')} ·
${UI.icon('check-circle')} ${UI.escape(r.resolved_by_name||'?')} · ${(r.resolved_at||'').slice(0,10)}`);
// --- POI-Korrekturen ---
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1831,11 +1831,11 @@ window.Page_admin = (() => {
<tbody>
${poiPending.map((e, i) => `
<tr style="${i%2===1?'background:var(--c-surface-2)':''}">
<td class="adm-td" style="font-weight:var(--weight-semibold)">${_esc(e.poi_name || `OSM #${e.osm_id}`)}</td>
<td class="adm-td"><code class="text-xs">${_esc(e.field)}</code></td>
<td class="adm-td" style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(e.old_value || '—')}</td>
<td class="adm-td text-xs">${_esc(e.new_value || '—')}</td>
<td class="adm-td text-muted">${_esc(e.einreicher_name || '?')}</td>
<td class="adm-td" style="font-weight:var(--weight-semibold)">${UI.escape(e.poi_name || `OSM #${e.osm_id}`)}</td>
<td class="adm-td"><code class="text-xs">${UI.escape(e.field)}</code></td>
<td class="adm-td" style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(e.old_value || '—')}</td>
<td class="adm-td text-xs">${UI.escape(e.new_value || '—')}</td>
<td class="adm-td text-muted">${UI.escape(e.einreicher_name || '?')}</td>
<td class="adm-td">${_ageLabel(e.created_at)}</td>
<td class="adm-td" style="text-align:right;white-space:nowrap">
<button class="btn btn-sm btn-primary adm-poi-approve" data-id="${e.id}" style="margin-right:4px">
@ -1852,12 +1852,12 @@ window.Page_admin = (() => {
}
// POI-History
if (poiDone.length) html += _historySection('POI-Korrekturen', poiDone,
e => `<span style="font-weight:600">${_esc(e.poi_name||`OSM #${e.osm_id}`)}</span> ·
<code class="text-xs">${_esc(e.field)}</code>:
<span style="text-decoration:line-through;color:var(--c-text-muted)">${_esc(e.old_value||'—')}</span>
${_esc(e.new_value||'—')} ·
e => `<span style="font-weight:600">${UI.escape(e.poi_name||`OSM #${e.osm_id}`)}</span> ·
<code class="text-xs">${UI.escape(e.field)}</code>:
<span style="text-decoration:line-through;color:var(--c-text-muted)">${UI.escape(e.old_value||'—')}</span>
${UI.escape(e.new_value||'—')} ·
${e.status==='approved' ? `${UI.icon('check-circle')} freigegeben` : `${UI.icon('x-circle')} abgelehnt`}
${e.mod_name ? ` von ${_esc(e.mod_name)}` : ''} · ${(e.resolved_at||'').slice(0,10)}`);
${e.mod_name ? ` von ${UI.escape(e.mod_name)}` : ''} · ${(e.resolved_at||'').slice(0,10)}`);
el.innerHTML = html;
@ -1990,17 +1990,17 @@ window.Page_admin = (() => {
<div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);
color:var(--c-text);margin-bottom:var(--space-1)">
${_esc(a.name)}
${UI.escape(a.name)}
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-weight:400;margin-left:6px">
${_esc(a.email)}
${UI.escape(a.email)}
</span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:var(--space-3);font-size:var(--text-xs);
color:var(--c-text-secondary);margin-bottom:var(--space-2)">
<span>${UI.icon('paw-print')} ${_esc(a.rasse_text || '')}</span>
<span>${UI.icon('house-line')} ${_esc(a.zwingername || '')}</span>
<span>${UI.icon('users')} ${_esc(a.verein || '')}</span>
<span>${UI.icon('map-pin')} ${_esc(a.stadt || '')}</span>
<span>${UI.icon('paw-print')} ${UI.escape(a.rasse_text || '')}</span>
<span>${UI.icon('house-line')} ${UI.escape(a.zwingername || '')}</span>
<span>${UI.icon('users')} ${UI.escape(a.verein || '')}</span>
<span>${UI.icon('map-pin')} ${UI.escape(a.stadt || '')}</span>
<span style="color:${a.vdh_mitglied ? 'var(--c-success)' : 'var(--c-text-muted)'}">
${UI.icon('certificate')} VDH: ${a.vdh_mitglied ? 'ja' : 'nein'}
</span>
@ -2010,7 +2010,7 @@ window.Page_admin = (() => {
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-sm);margin-top:var(--space-1)">
${_esc(a.beschreibung)}
${UI.escape(a.beschreibung)}
</div>` : ''}
</div>
@ -2021,11 +2021,11 @@ window.Page_admin = (() => {
${UI.icon('file-text')} Dokumente
</button>
<button class="btn btn-sm btn-primary adm-breeder-approve"
data-uid="${a.user_id || a.id}" data-name="${_esc(a.name)}">
data-uid="${a.user_id || a.id}" data-name="${UI.escape(a.name)}">
${UI.icon('check')} Freischalten
</button>
<button class="btn btn-sm btn-ghost adm-breeder-reject"
data-uid="${a.user_id || a.id}" data-name="${_esc(a.name)}"
data-uid="${a.user_id || a.id}" data-name="${UI.escape(a.name)}"
class="text-danger">
${UI.icon('x')} Ablehnen
</button>
@ -2053,11 +2053,11 @@ window.Page_admin = (() => {
body: docs.length
? `<div class="flex-col-gap-3">
${docs.map(d => `
<a href="${_esc(API.breeder.documentUrl(uid, d.id))}"
<a href="${UI.escape(API.breeder.documentUrl(uid, d.id))}"
target="_blank" rel="noopener"
class="btn btn-secondary"
style="text-align:left;word-break:break-all">
${UI.icon('file')} ${_esc(d.filename || d.name || 'Dokument ' + d.id)}
${UI.icon('file')} ${UI.escape(d.filename || d.name || 'Dokument ' + d.id)}
</a>`).join('')}
</div>`
: `<p class="text-sm-muted">Keine Dokumente hochgeladen.</p>`,
@ -2155,12 +2155,12 @@ window.Page_admin = (() => {
const rows = breeders.map(b => `
<tr>
<td style="padding:var(--space-2) var(--space-3)">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(b.name)}</div>
<div class="text-xs-muted">${_esc(b.email)}</div>
<div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(b.name)}</div>
<div class="text-xs-muted">${UI.escape(b.email)}</div>
</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(b.zwingername || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.rasse_text || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.stadt || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${UI.escape(b.zwingername || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(b.rasse_text || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(b.stadt || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);text-align:center;font-size:var(--text-xs)">
${b.wuerfe_count || 0} Würfe<br>
<span class="text-muted">${b.hunde_count || 0} Hunde</span>
@ -2171,7 +2171,7 @@ window.Page_admin = (() => {
</td>
<td style="padding:var(--space-2) var(--space-3)">
<button class="btn btn-sm btn-ghost adm-breeder-tier-btn"
data-uid="${b.id}" data-name="${_esc(b.name)}" data-tier="${_esc(b.subscription_tier || 'standard')}"
data-uid="${b.id}" data-name="${UI.escape(b.name)}" data-tier="${UI.escape(b.subscription_tier || 'standard')}"
class="text-xs">
Abo
</button>
@ -2241,17 +2241,17 @@ window.Page_admin = (() => {
${jobs.map((j, i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td class="adm-td" style="font-weight:var(--weight-semibold);color:var(--c-text)">
${_esc(j.name)}
<div class="adm-job-id">${_esc(j.id)}</div>
${UI.escape(j.name)}
<div class="adm-job-id">${UI.escape(j.id)}</div>
</td>
<td class="adm-td" style="color:var(--c-text-secondary);white-space:nowrap">
${j.next_run_time ? _formatDateTime(j.next_run_time) : '<span class="text-muted">—</span>'}
</td>
<td class="adm-td adm-td-trigger">
${_esc(j.trigger)}
${UI.escape(j.trigger)}
</td>
<td class="adm-td text-right">
<button class="btn btn-sm btn-ghost adm-job-trigger adm-icon-btn" data-id="${_esc(j.id)}" data-name="${_esc(j.name)}"
<button class="btn btn-sm btn-ghost adm-job-trigger adm-icon-btn" data-id="${UI.escape(j.id)}" data-name="${UI.escape(j.name)}"
title="Jetzt ausführen" class="text-primary">
${UI.icon('play')}
</button>
@ -2542,11 +2542,11 @@ window.Page_admin = (() => {
background:var(--c-bg-elevated);border-radius:var(--radius-md);border:1px solid var(--c-border)">
<div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2)">
<span style="font-size:var(--text-sm);font-weight:600">${_esc(t.label)}</span>
<span style="font-size:var(--text-sm);font-weight:600">${UI.escape(t.label)}</span>
${accountBadge(t.from_account)}
</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(t.subject)}
${UI.escape(t.subject)}
</div>
</div>
<div style="display:flex;gap:var(--space-2);flex-shrink:0">
@ -2632,9 +2632,9 @@ window.Page_admin = (() => {
onmouseover="this.style.background='var(--c-surface-2)'"
onmouseout="this.style.background=''">
<td class="p-2">${accountBadge(l.from_account)}</td>
<td class="p-2">${_esc(l.recipient)}</td>
<td style="padding:var(--space-2);color:var(--c-text-secondary)">${_esc(l.subject)}</td>
<td style="padding:var(--space-2);color:var(--c-text-muted)">${_esc(l.sent_by_name || '')}</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>
<td style="padding:var(--space-2);color:var(--c-text-muted)">${UI.escape(l.sent_by_name || '')}</td>
<td style="padding:var(--space-2);color:var(--c-text-muted)">${(l.sent_at||'').slice(0,16).replace('T',' ')}</td>
</tr>`).join('')}
</tbody>
@ -2650,17 +2650,17 @@ window.Page_admin = (() => {
const l = log[Number(row.dataset.logIdx)];
if (!l) return;
UI.modal.open({
title: _esc(l.subject),
title: UI.escape(l.subject),
body: `
<div style="margin-bottom:var(--space-3);font-size:var(--text-sm);color:var(--c-text-muted)">
<strong>An:</strong> ${_esc(l.recipient)} &nbsp;·&nbsp;
<strong>Von:</strong> ${_esc(l.from_account)}@banyaro.app &nbsp;·&nbsp;
<strong>An:</strong> ${UI.escape(l.recipient)} &nbsp;·&nbsp;
<strong>Von:</strong> ${UI.escape(l.from_account)}@banyaro.app &nbsp;·&nbsp;
${(l.sent_at||'').slice(0,16).replace('T',' ')}
</div>
<pre style="white-space:pre-wrap;font-family:inherit;font-size:var(--text-sm);
background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-3);max-height:60vh;overflow-y:auto;
color:var(--c-text)">${_esc(l.body || '(kein Text gespeichert)')}</pre>`,
color:var(--c-text)">${UI.escape(l.body || '(kein Text gespeichert)')}</pre>`,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
});
});
@ -2733,7 +2733,7 @@ window.Page_admin = (() => {
<div>
<label class="form-label text-xs">Name (intern)</label>
<input class="form-control" id="${id}-key" type="text" placeholder="z.B. willkommen_neu"
value="${_esc(tpl?.key || '')}" ${isNew ? '' : 'readonly'}>
value="${UI.escape(tpl?.key || '')}" ${isNew ? '' : 'readonly'}>
</div>
<div>
<label class="form-label text-xs">Absender</label>
@ -2746,17 +2746,17 @@ window.Page_admin = (() => {
<div>
<label class="form-label text-xs">Bezeichnung (sichtbar)</label>
<input class="form-control" id="${id}-label" type="text" placeholder="z.B. Willkommensnachricht"
value="${_esc(tpl?.label || '')}">
value="${UI.escape(tpl?.label || '')}">
</div>
<div>
<label class="form-label text-xs">Betreff</label>
<input class="form-control" id="${id}-subject" type="text"
value="${_esc(tpl?.subject || '')}">
value="${UI.escape(tpl?.subject || '')}">
</div>
<div>
<label class="form-label text-xs">Text</label>
<textarea id="${id}-body" class="form-control" rows="12"
style="font-family:monospace;font-size:var(--text-sm);resize:vertical">${_esc(tpl?.body || '')}</textarea>
style="font-family:monospace;font-size:var(--text-sm);resize:vertical">${UI.escape(tpl?.body || '')}</textarea>
</div>
</form>`,
footer: `
@ -2826,14 +2826,14 @@ window.Page_admin = (() => {
${_formatDateTime(r.created_at)}
</td>
<td class="adm-td" style="color:var(--c-text);white-space:nowrap">
${_esc(r.admin_name || '—')}
${UI.escape(r.admin_name || '—')}
</td>
<td class="adm-td">
<span class="adm-badge-mono">${_esc(r.action)}</span>
${r.detail ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(r.detail)}</div>` : ''}
<span class="adm-badge-mono">${UI.escape(r.action)}</span>
${r.detail ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(r.detail)}</div>` : ''}
</td>
<td class="adm-td" style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:nowrap">
${_esc(r.target || '—')}
${UI.escape(r.target || '—')}
</td>
</tr>
`).join('')}
@ -2889,11 +2889,6 @@ window.Page_admin = (() => {
`;
}
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ------------------------------------------------------------------
// BEWERBUNGEN — Social-Media-Job
// ------------------------------------------------------------------
@ -2932,19 +2927,19 @@ window.Page_admin = (() => {
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-4)" data-id="${r.id}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3)">
<div class="flex-1-min">
<div style="font-weight:700;font-size:var(--text-base)">${_esc(r.name)}
${r.username ? `<span style="color:var(--c-text-muted);font-weight:400;font-size:var(--text-sm)">(@${_esc(r.username)})</span>` : ''}
<div style="font-weight:700;font-size:var(--text-base)">${UI.escape(r.name)}
${r.username ? `<span style="color:var(--c-text-muted);font-weight:400;font-size:var(--text-sm)">(@${UI.escape(r.username)})</span>` : ''}
</div>
<div style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-top:2px">
${_esc(r.email)} · @${_esc(r.social_handle||'—')}
${r.dog_name ? ` · 🐕 ${_esc(r.dog_name)} (${_esc(r.dog_rasse||'')})` : ''}
${UI.escape(r.email)} · @${UI.escape(r.social_handle||'—')}
${r.dog_name ? ` · 🐕 ${UI.escape(r.dog_name)} (${UI.escape(r.dog_rasse||'')})` : ''}
</div>
<div style="color:var(--c-text-muted);font-size:var(--text-xs);margin-top:2px">
${r.created_at?.slice(0,16).replace('T',' ')} · ${r.doc_count} Anhang/Anhänge
</div>
<div style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);
background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3)">
${_esc((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''}
${UI.escape((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''}
</div>
</div>
<div style="display:flex;flex-direction:column;gap:var(--space-2);min-width:120px">
@ -2976,25 +2971,25 @@ window.Page_admin = (() => {
const docsHtml = app.docs?.length
? app.docs.map(d => `<a href="/api/jobs/admin/applications/${id}/docs/${d.id}"
target="_blank" style="display:block;color:var(--c-primary);font-size:var(--text-sm);margin:4px 0">
📎 ${_esc(d.filename)}</a>`).join('')
📎 ${UI.escape(d.filename)}</a>`).join('')
: '<span class="text-sm-muted">Keine Anhänge</span>';
UI.modal.open({
title: `Bewerbung — ${_esc(app.name)}`,
title: `Bewerbung — ${UI.escape(app.name)}`,
body: `
<div style="display:grid;gap:var(--space-3)">
<div><b>E-Mail:</b> ${_esc(app.email)}</div>
<div><b>Social:</b> @${_esc(app.social_handle||'')}</div>
${app.dog_name ? `<div><b>Hund:</b> ${_esc(app.dog_name)} (${_esc(app.dog_rasse||'')})</div>` : ''}
<div><b>E-Mail:</b> ${UI.escape(app.email)}</div>
<div><b>Social:</b> @${UI.escape(app.social_handle||'')}</div>
${app.dog_name ? `<div><b>Hund:</b> ${UI.escape(app.dog_name)} (${UI.escape(app.dog_rasse||'')})</div>` : ''}
<div><b>Motivation:</b><br>
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3);
margin-top:var(--space-1);font-size:var(--text-sm);white-space:pre-wrap">${_esc(app.motivation)}</div>
margin-top:var(--space-1);font-size:var(--text-sm);white-space:pre-wrap">${UI.escape(app.motivation)}</div>
</div>
<div><b>Anhänge:</b><br>${docsHtml}</div>
<div>
<b>Admin-Notiz:</b>
<textarea id="adm-bew-note" class="form-control" rows="2" style="margin-top:var(--space-1)"
placeholder="Interne Notiz / Nachricht an Bewerber">${_esc(app.admin_note||'')}</textarea>
placeholder="Interne Notiz / Nachricht an Bewerber">${UI.escape(app.admin_note||'')}</textarea>
</div>
</div>`,
footer: `
@ -3052,7 +3047,7 @@ window.Page_admin = (() => {
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
${Object.entries(KAT_LABEL).map(([k,v]) =>
`<option value="${k}">${_esc(v)}</option>`
`<option value="${k}">${UI.escape(v)}</option>`
).join('')}
</select>
</div>
@ -3125,7 +3120,7 @@ window.Page_admin = (() => {
text-transform:uppercase;letter-spacing:0.05em;
padding:var(--space-2) 0;margin-bottom:var(--space-2);
border-bottom:1px solid var(--c-border)">
${_esc(label)}
${UI.escape(label)}
</div>
`;
for (const a of items) {
@ -3138,7 +3133,7 @@ window.Page_admin = (() => {
padding:var(--space-3) var(--space-4)">
<span style="flex:1;font-size:var(--text-sm);font-weight:500;
${a.aktiv ? '' : 'opacity:0.45;text-decoration:line-through'}">
${_esc(a.frage)}
${UI.escape(a.frage)}
</span>
<span style="font-size:var(--text-xs);color:var(--c-text-muted);
white-space:nowrap">
@ -3159,7 +3154,7 @@ window.Page_admin = (() => {
<button class="btn btn-sm adm-hilfe-del-btn"
style="padding:2px 8px;font-size:var(--text-xs);
background:var(--c-danger-bg,#fee2e2);color:var(--c-danger,#991b1b)"
data-id="${a.id}" data-frage="${_esc(a.frage)}">
data-id="${a.id}" data-frage="${UI.escape(a.frage)}">
${UI.icon('trash')}
</button>
</div>
@ -3177,7 +3172,7 @@ window.Page_admin = (() => {
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
${Object.entries(KAT_LABEL).map(([k,v]) =>
`<option value="${k}" ${k === a.kategorie ? 'selected' : ''}>${_esc(v)}</option>`
`<option value="${k}" ${k === a.kategorie ? 'selected' : ''}>${UI.escape(v)}</option>`
).join('')}
</select>
</div>
@ -3185,7 +3180,7 @@ window.Page_admin = (() => {
<label style="font-size:var(--text-xs);font-weight:600;display:block;
margin-bottom:4px;color:var(--c-text-secondary)">Frage</label>
<input type="text" class="adm-hilfe-edit-frage"
value="${_esc(a.frage)}"
value="${UI.escape(a.frage)}"
style="width:100%;padding:var(--space-2) var(--space-3);
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text);
@ -3199,7 +3194,7 @@ window.Page_admin = (() => {
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text);
font-size:var(--text-sm);box-sizing:border-box;
resize:vertical;font-family:inherit">${_esc(a.antwort)}</textarea>
resize:vertical;font-family:inherit">${UI.escape(a.antwort)}</textarea>
</div>
<div style="display:flex;align-items:center;gap:var(--space-3)">
<label style="font-size:var(--text-xs);font-weight:600;
@ -3494,8 +3489,8 @@ window.Page_admin = (() => {
const topRows = d.top_referrers.map((r, i) => `
<tr>
<td style="padding:8px 10px;color:var(--c-text-muted);font-weight:600">${i + 1}</td>
<td style="padding:8px 10px;font-weight:600">${_esc(r.name)}</td>
<td style="padding:8px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(r.email)}</td>
<td style="padding:8px 10px;font-weight:600">${UI.escape(r.name)}</td>
<td style="padding:8px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${UI.escape(r.email)}</td>
<td style="padding:8px 10px;text-align:right">
<span style="font-size:var(--text-lg);font-weight:800;color:var(--c-primary)">${r.invited_count}</span>
</td>
@ -3503,8 +3498,8 @@ window.Page_admin = (() => {
const recentRows = d.recent_invites.slice(0, 50).map(r => `
<tr>
<td style="padding:6px 10px;font-weight:500">${_esc(r.name)}</td>
<td style="padding:6px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(r.referrer_name)}</td>
<td style="padding:6px 10px;font-weight:500">${UI.escape(r.name)}</td>
<td style="padding:6px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${UI.escape(r.referrer_name)}</td>
<td style="padding:6px 10px;color:var(--c-text-muted);font-size:var(--text-xs)">${(r.created_at || '').slice(0, 10)}</td>
</tr>`).join('');
@ -3575,8 +3570,8 @@ window.Page_admin = (() => {
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap">
<div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(r.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">${_esc(r.email)}</div>
<div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(r.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">${UI.escape(r.email)}</div>
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
${tierBadge(r.tier)}
${r.discount_pct > 0 ? `<span style="display:inline-block;padding:1px 8px;border-radius:999px;
@ -3590,7 +3585,7 @@ window.Page_admin = (() => {
${r.message ? `<div style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
background:var(--c-surface-raised,rgba(0,0,0,.04))">
${_esc(r.message)}
${UI.escape(r.message)}
</div>` : ''}
</div>
</div>
@ -3598,15 +3593,15 @@ window.Page_admin = (() => {
${r.existing_invoice_id ? `
<button class="btn adm-invoice-edit-btn"
data-invoice-id="${r.existing_invoice_id}"
title="Rechnung ${_esc(r.existing_invoice_number)} (${_esc(r.existing_invoice_status)}) bearbeiten"
title="Rechnung ${UI.escape(r.existing_invoice_number)} (${UI.escape(r.existing_invoice_status)}) bearbeiten"
style="background:#eab308;color:#1a1a1a;border:none;
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
cursor:pointer;font-size:var(--text-sm);font-weight:600">
${UI.icon('receipt')} Rechnung bearbeiten
</button>` : `
<button class="btn adm-invoice-btn"
data-name="${_esc(r.name)}" data-email="${_esc(r.email)}"
data-tier="${r.tier}" data-address="${_esc(r.billing_address || '')}"
data-name="${UI.escape(r.name)}" data-email="${UI.escape(r.email)}"
data-tier="${r.tier}" data-address="${UI.escape(r.billing_address || '')}"
data-discount="${r.discount_pct || 0}"
data-discount-reason="${r.discount_reason || ''}"
data-referral-count="${r.referral_count || 0}"
@ -3615,7 +3610,7 @@ window.Page_admin = (() => {
cursor:pointer;font-size:var(--text-sm);font-weight:600">
${UI.icon('receipt')} Rechnung erstellen
</button>`}
<button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}"
<button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${UI.escape(r.name)}" data-tier="${r.tier}"
style="background:#16a34a;color:#fff;border:none;
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
cursor:pointer;font-size:var(--text-sm);font-weight:600">
@ -3627,8 +3622,8 @@ window.Page_admin = (() => {
// Erledigte als kompakte Tabellenzeilen
const _doneRow = r => `
<tr>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(r.name)}<br>
<span class="text-xs-muted">${_esc(r.email)}</span></td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${UI.escape(r.name)}<br>
<span class="text-xs-muted">${UI.escape(r.email)}</span></td>
<td style="padding:var(--space-2) var(--space-3)">${tierBadge(r.tier)}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-success)">
${r.fulfilled_at?.slice(0,10) || ''}</td>
@ -3841,12 +3836,12 @@ window.Page_admin = (() => {
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-edit" data-id="${inv.id}" title="Bearbeiten">
${UI.icon('pencil')} Bearbeiten
</button>`);
actions.push(`<button class="btn btn-sm btn-primary adm-inv-send" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" title="Senden">
actions.push(`<button class="btn btn-sm btn-primary adm-inv-send" data-id="${inv.id}" data-num="${UI.escape(inv.invoice_number)}" title="Senden">
${UI.icon('paper-plane-tilt')} Senden
</button>`);
}
if (inv.status === 'sent') {
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-send" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" title="Erneut senden"
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-send" data-id="${inv.id}" data-num="${UI.escape(inv.invoice_number)}" title="Erneut senden"
class="text-muted">
${UI.icon('paper-plane-tilt')} Erneut senden
</button>`);
@ -3855,7 +3850,7 @@ window.Page_admin = (() => {
actions.push(`<button class="btn btn-sm btn-secondary adm-inv-pay" data-id="${inv.id}" data-amount="${inv.amount_gross}" title="Als bezahlt markieren">
${UI.icon('check-circle')} Bezahlt
</button>`);
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-cancel" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}"
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-cancel" data-id="${inv.id}" data-num="${UI.escape(inv.invoice_number)}"
class="text-danger" title="Stornieren">
${UI.icon('x-circle')} Storno
</button>`);
@ -3869,11 +3864,11 @@ window.Page_admin = (() => {
return `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td class="adm-td" style="font-weight:600;font-family:monospace;font-size:var(--text-xs)">
${_esc(inv.invoice_number)}
${UI.escape(inv.invoice_number)}
</td>
<td class="adm-td">
<div style="font-weight:500">${_esc(inv.recipient_name)}</div>
<div class="text-xs-muted">${_esc(inv.recipient_email || '')}</div>
<div style="font-weight:500">${UI.escape(inv.recipient_name)}</div>
<div class="text-xs-muted">${UI.escape(inv.recipient_email || '')}</div>
</td>
<td class="adm-td" style="text-align:right;font-weight:700;white-space:nowrap">
${_fmtEur(inv.amount_gross)}
@ -4007,12 +4002,12 @@ window.Page_admin = (() => {
<div>
<label class="form-label text-xs">Empfänger Name *</label>
<input class="form-control" name="recipient_name" type="text" required
placeholder="Max Muster" value="${_esc(p.recipient_name || '')}">
placeholder="Max Muster" value="${UI.escape(p.recipient_name || '')}">
</div>
<div>
<label class="form-label text-xs">E-Mail</label>
<input class="form-control" name="recipient_email" type="email"
placeholder="max@example.com" value="${_esc(p.recipient_email || '')}">
placeholder="max@example.com" value="${UI.escape(p.recipient_email || '')}">
</div>
</div>
@ -4024,14 +4019,14 @@ window.Page_admin = (() => {
</label>
<textarea class="form-control" name="recipient_address" rows="2"
placeholder="Musterstr. 1&#10;12345 Berlin"
style="resize:vertical;font-family:inherit">${_esc(p.recipient_address || '')}</textarea>
style="resize:vertical;font-family:inherit">${UI.escape(p.recipient_address || '')}</textarea>
</div>
<div>
<label class="form-label text-xs">Leistungszeitraum <span class="text-muted">(optional)</span></label>
<input class="form-control" name="service_period" type="text"
placeholder="z.B. 15.05.2026 oder einmalige Leistung"
value="${_esc(p.service_period || '')}">
value="${UI.escape(p.service_period || '')}">
</div>
<!-- Positionen -->
@ -4066,7 +4061,7 @@ window.Page_admin = (() => {
<label class="form-label text-xs">Notizen <span class="text-muted">(optional)</span></label>
<textarea class="form-control" name="notes" rows="2"
style="resize:vertical;font-family:inherit"
placeholder="Interne Notiz / Zahlungshinweis">${_esc(p.notes || (!isEdit && !p.recipient_name ? 'Zahlbar innerhalb von 14 Tagen ab Rechnungsdatum.' : ''))}</textarea>
placeholder="Interne Notiz / Zahlungshinweis">${UI.escape(p.notes || (!isEdit && !p.recipient_name ? 'Zahlbar innerhalb von 14 Tagen ab Rechnungsdatum.' : ''))}</textarea>
</div>
</form>
@ -4120,7 +4115,7 @@ window.Page_admin = (() => {
itemEl.style.cssText = 'display:grid;grid-template-columns:1fr 60px 100px auto;gap:var(--space-2);align-items:center';
itemEl.innerHTML = `
<input class="form-control inv-item-desc" type="text" placeholder="Beschreibung *"
value="${_esc(desc)}" class="text-sm">
value="${UI.escape(desc)}" class="text-sm">
<input class="form-control inv-item-qty" type="number" min="1" value="${qty}"
style="font-size:var(--text-sm);text-align:right" title="Menge">
<input class="form-control inv-item-price" type="number" min="0" step="0.01" value="${price.toFixed(2)}"
@ -4303,7 +4298,7 @@ window.Page_admin = (() => {
body: `
<form id="${id}" class="flex-col-gap-3">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0">
Rechnung <strong>${_esc(invoiceNum)}</strong> stornieren.
Rechnung <strong>${UI.escape(invoiceNum)}</strong> stornieren.
</p>
<div>
<label class="form-label text-xs">Stornierungsgrund *</label>
@ -4360,7 +4355,7 @@ window.Page_admin = (() => {
const itemsHtml = (inv.items || []).map(item => `
<tr>
<td style="padding:6px 8px">${_esc(item.description)}</td>
<td style="padding:6px 8px">${UI.escape(item.description)}</td>
<td style="padding:6px 8px;text-align:right">${item.quantity}</td>
<td style="padding:6px 8px;text-align:right">${_fmtEur(item.unit_price)}</td>
<td style="padding:6px 8px;text-align:right;font-weight:600">${_fmtEur(item.total)}</td>
@ -4368,15 +4363,15 @@ window.Page_admin = (() => {
`).join('');
UI.modal.open({
title: `${UI.icon('receipt')} ${_esc(inv.invoice_number)}`,
title: `${UI.icon('receipt')} ${UI.escape(inv.invoice_number)}`,
body: `
<div style="display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--text-sm)">
<div class="grid-2">
<div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Empfänger</div>
<div style="font-weight:600">${_esc(inv.recipient_name)}</div>
${inv.recipient_email ? `<div style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(inv.recipient_email)}</div>` : ''}
${inv.recipient_address ? `<div style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:pre-line;margin-top:2px">${_esc(inv.recipient_address)}</div>` : ''}
<div style="font-weight:600">${UI.escape(inv.recipient_name)}</div>
${inv.recipient_email ? `<div style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(inv.recipient_email)}</div>` : ''}
${inv.recipient_address ? `<div style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:pre-line;margin-top:2px">${UI.escape(inv.recipient_address)}</div>` : ''}
</div>
<div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Status</div>
@ -4392,7 +4387,7 @@ window.Page_admin = (() => {
${inv.service_period ? `
<div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Leistungszeitraum</div>
<div>${_esc(inv.service_period)}</div>
<div>${UI.escape(inv.service_period)}</div>
</div>` : ''}
<!-- Positionen -->
@ -4421,7 +4416,7 @@ window.Page_admin = (() => {
<div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Notizen</div>
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
font-size:var(--text-xs);white-space:pre-wrap">${_esc(inv.notes)}</div>
font-size:var(--text-xs);white-space:pre-wrap">${UI.escape(inv.notes)}</div>
</div>` : ''}
</div>
`,
@ -4451,7 +4446,7 @@ window.Page_admin = (() => {
const monthRows = (cf.monthly || []).map((m, i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td class="adm-td">${_esc(m.month)}</td>
<td class="adm-td">${UI.escape(m.month)}</td>
<td class="adm-td text-right">${m.count}</td>
<td class="adm-td" style="text-align:right;font-weight:600">${_fmtEur(m.revenue)}</td>
</tr>`).join('');
@ -4593,8 +4588,8 @@ window.Page_admin = (() => {
? ` <span class="text-xs-muted">(RG: ${_fmtE(inv.amount_gross)})</span>` : '';
return `
<tr style="${i%2===1?'background:var(--c-surface-2)':''}">
<td class="adm-td" style="font-family:monospace;font-size:var(--text-xs);${isStorno?'color:var(--c-danger)':''}">${_esc(inv.invoice_number)}</td>
<td class="adm-td">${_esc(inv.recipient_name)}</td>
<td class="adm-td" style="font-family:monospace;font-size:var(--text-xs);${isStorno?'color:var(--c-danger)':''}">${UI.escape(inv.invoice_number)}</td>
<td class="adm-td">${UI.escape(inv.recipient_name)}</td>
<td class="adm-td" style="text-align:right;font-weight:600;${amtColor}">${_fmtE(effectiveAmt)}${amtNote}</td>
<td class="adm-td" style="${isStorno?'color:var(--c-danger)':''}">${sL[inv.status]||inv.status}</td>
<td class="adm-td text-xs-muted">${_fmtD(inv.created_at)}</td>
@ -4602,7 +4597,7 @@ window.Page_admin = (() => {
}).join('');
resultEl.innerHTML = `
<div style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary);margin-bottom:var(--space-2)">
${_esc(data.period || `Q${q} ${year}`)} ${data.count} Buchung(en) · Summe: ${_fmtE(data.total_gross)}
${UI.escape(data.period || `Q${q} ${year}`)} ${data.count} Buchung(en) · Summe: ${_fmtE(data.total_gross)}
</div>
<div class="adm-table-scroll">
<table class="adm-table">
@ -4622,7 +4617,7 @@ window.Page_admin = (() => {
</table>
</div>`;
} catch (e) {
resultEl.innerHTML = `<div style="color:var(--c-danger);font-size:var(--text-xs)">Fehler: ${_esc(e.message)}</div>`;
resultEl.innerHTML = `<div style="color:var(--c-danger);font-size:var(--text-xs)">Fehler: ${UI.escape(e.message)}</div>`;
}
});
}