Partner-Freigabe: Live-Vorschau im Admin + Mail-Fehler sichtbar machen

Rene: 'kann nichts prüfen — ich würde den Output erwarten, der auf der
Partner-Seite zu sehen sein wird'. Freigabe-Zeile hat jetzt einen
Vorschau-Toggle, der die Karte 1:1 wie auf #partner rendert (Logo/Initial,
Slogan, Website, Instagram, Bio, Medien-Grid).

Mail-Ursache gefunden: Staging-.env fehlte SMTP_SUPPORT_USER → Code-Default
support@banyaro.de → 535 Auth-Fehler, vom Silent-Catch verschluckt.
.env ergänzt (partner@banyaro.app wie Prod); Submit loggt SMTP-Fehler jetzt
über _log_smtp_failure in failed_emails statt still zu schlucken.
This commit is contained in:
rene 2026-06-07 17:43:42 +02:00
parent a40aa183ec
commit cadfb24a8d
7 changed files with 84 additions and 42 deletions

View file

@ -481,20 +481,18 @@ def submit_partner_profile(user=Depends(require_partner)):
(user["id"],)
)
profile = _pp_get_or_empty(conn, user["id"])
# Admin benachrichtigen (best effort — Silent-Skip ohne ADMIN_EMAIL)
# Admin benachrichtigen — Fehler landen in failed_emails (Admin-Retry), kein Silent-Skip
admin_email = os.getenv("ADMIN_EMAIL", "")
if admin_email and profile.get("approved") != 1:
subject = f"[Ban Yaro] Partner-Profil eingereicht: {profile.get('display_name')}"
body = (f"Partner {user['name']} ({user['email']}) hat sein Profil zur "
f"Freigabe eingereicht.\n\nAdmin-Panel: https://banyaro.app/#admin")
try:
from routes.outreach import _send_smtp
_send_smtp(
admin_email,
f"[Ban Yaro] Partner-Profil eingereicht: {profile.get('display_name')}",
(f"Partner {user['name']} ({user['email']}) hat sein Profil zur "
f"Freigabe eingereicht.\n\nAdmin-Panel: https://banyaro.app/#admin"),
"support",
)
except Exception:
pass
_send_smtp(admin_email, subject, body, "support")
except Exception as exc:
from routes.auth import _log_smtp_failure
_log_smtp_failure(admin_email, subject, body, exc, context="partner_profile_submit")
return {"profile": profile}

View file

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

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1255'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1256'; // ← 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;

View file

@ -2395,23 +2395,60 @@ window.Page_admin = (() => {
${profiles.length === 0
? `<p class="text-sm-muted">Noch keine Partner-Profile angelegt.</p>`
: profiles.map(p => `
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--c-border)" data-pp-uid="${p.user_id}">
${p.logo_url
? `<img src="${UI.escape(p.logo_url)}" style="width:36px;height:36px;border-radius:var(--radius-md);object-fit:contain;background:var(--c-surface-2);flex-shrink:0">`
: `<div style="width:36px;height:36px;border-radius:var(--radius-md);background:var(--c-surface-2);flex-shrink:0"></div>`}
<div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(p.display_name || p.name)}</div>
<div class="text-xs-muted">${UI.escape(p.name)} · ${UI.escape(p.email)}${p.photos?.length ? ` · ${p.photos.length} Medien` : ''}</div>
<div style="border-bottom:1px solid var(--c-border)" data-pp-uid="${p.user_id}">
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0">
${p.logo_url
? `<img src="${UI.escape(p.logo_url)}" style="width:36px;height:36px;border-radius:var(--radius-md);object-fit:contain;background:var(--c-surface-2);flex-shrink:0">`
: `<div style="width:36px;height:36px;border-radius:var(--radius-md);background:var(--c-surface-2);flex-shrink:0"></div>`}
<div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(p.display_name || p.name)}</div>
<div class="text-xs-muted">${UI.escape(p.name)} · ${UI.escape(p.email)}${p.photos?.length ? ` · ${p.photos.length} Medien` : ''}</div>
</div>
${p.approved === 1
? `<span class="badge" style="background:#dcfce7;color:#16a34a">✓ Frei</span>`
: p.approved === -1
? `<span class="badge" style="background:#fee2e2;color:#dc2626">✗ Abgelehnt</span>`
: p.submitted_at
? `<span class="badge" style="background:#fef9c3;color:#a16207">⏳ Prüfen</span>`
: `<span class="badge">Entwurf</span>`}
<button class="btn btn-sm btn-secondary adm-pp-preview" data-uid="${p.user_id}">
${UI.icon('eye')} Vorschau
</button>
${p.approved !== 1 ? `<button class="btn btn-sm btn-primary adm-pp-review" data-uid="${p.user_id}" data-val="1">✓ Freigeben</button>` : ''}
${p.approved !== -1 ? `<button class="btn btn-sm btn-ghost adm-pp-review text-danger" data-uid="${p.user_id}" data-val="-1">✗</button>` : ''}
</div>
<!-- Vorschau: so erscheint die Karte auf der öffentlichen Partner-Seite -->
<div class="adm-pp-preview-card hidden" id="adm-pp-preview-${p.user_id}" style="margin:0 0 var(--space-3)">
<div class="text-xs-muted" style="margin-bottom:var(--space-2)">
${UI.icon('eye')} So erscheint die Karte auf der Partner-Seite:
</div>
<div class="by-card" style="padding:var(--space-4);position:relative;overflow:hidden;max-width:340px">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(135deg,#7c3aed,#a855f7)"></div>
<div style="display:flex;align-items:center;gap:var(--space-3)">
${p.logo_url
? `<img src="${UI.escape(p.logo_url)}" alt="" style="width:56px;height:56px;border-radius:var(--radius-md);object-fit:contain;flex-shrink:0;background:var(--c-surface-2);padding:4px">`
: `<div style="width:56px;height:56px;border-radius:50%;flex-shrink:0;background:linear-gradient(135deg,#7c3aed,#a855f7);display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:800;color:#fff">${UI.escape((p.display_name || p.name || '?')[0].toUpperCase())}</div>`}
<div class="flex-1-min">
<div style="font-weight:700;font-size:var(--text-base)">${UI.escape(p.display_name || p.name)}</div>
${p.tagline ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:1px">${UI.escape(p.tagline)}</div>` : ''}
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-1)">
${p.website ? `<a href="${UI.escape(p.website)}" target="_blank" rel="noopener" style="font-size:var(--text-xs);color:var(--c-primary)">🌐 ${UI.escape(p.website.replace(/^https?:\/\//, ''))}</a>` : ''}
${p.instagram ? `<span class="text-xs-muted">📸 ${UI.escape(p.instagram)}</span>` : ''}
</div>
</div>
</div>
${p.bio ? `<p style="margin:var(--space-3) 0 0;font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${UI.escape(p.bio)}</p>` : ''}
${p.photos?.length ? `
<div style="display:grid;grid-template-columns:repeat(${Math.min(p.photos.length, 3)},1fr);gap:var(--space-1);margin-top:var(--space-3);border-radius:var(--radius-md);overflow:hidden">
${p.photos.slice(0, 3).map(url => {
const isVid = url.endsWith('.mp4') || url.endsWith('.webm');
return isVid
? `<video src="${UI.escape(url)}" style="width:100%;aspect-ratio:1;object-fit:cover" muted playsinline loop autoplay></video>`
: `<img src="${UI.escape(url)}" style="width:100%;aspect-ratio:1;object-fit:cover">`;
}).join('')}
</div>` : ''}
</div>
</div>
${p.approved === 1
? `<span class="badge" style="background:#dcfce7;color:#16a34a">✓ Frei</span>`
: p.approved === -1
? `<span class="badge" style="background:#fee2e2;color:#dc2626">✗ Abgelehnt</span>`
: p.submitted_at
? `<span class="badge" style="background:#fef9c3;color:#a16207">⏳ Prüfen</span>`
: `<span class="badge">Entwurf</span>`}
${p.approved !== 1 ? `<button class="btn btn-sm btn-primary adm-pp-review" data-uid="${p.user_id}" data-val="1">✓</button>` : ''}
${p.approved !== -1 ? `<button class="btn btn-sm btn-ghost adm-pp-review text-danger" data-uid="${p.user_id}" data-val="-1">✗</button>` : ''}
</div>`).join('')}
</div>
@ -2446,6 +2483,13 @@ window.Page_admin = (() => {
</div>
`;
// Partner-Profil-Vorschau auf-/zuklappen (.hidden hat !important → classList)
el.querySelectorAll('.adm-pp-preview').forEach(btn => {
btn.addEventListener('click', () => {
el.querySelector(`#adm-pp-preview-${btn.dataset.uid}`)?.classList.toggle('hidden');
});
});
// Partner-Profil freigeben / ablehnen
el.querySelectorAll('.adm-pp-review').forEach(btn => {
btn.addEventListener('click', async () => {

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=1255"></script>
<script src="/js/landing-init.js?v=1256"></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, direkt im Browser.">
<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 = '1255';
const VER = '1256';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten