Compare commits

..

No commits in common. "74b6c03bb32b0bf9b0db4df0472a075ba8d11a68" and "ba5547f99386f42a9d4e100abe1931e4668db9f7" have entirely different histories.

8 changed files with 150 additions and 956 deletions

View file

@ -46,7 +46,7 @@ Maps: Leaflet.js + OpenStreetMap (kostenlos, kein Google-Lock)
--- ---
## Implementierungsstand (aktuell: 2026-04-24, SW by-v356, APP_VER 343) ## Implementierungsstand (aktuell: 2026-04-24, SW by-v333, APP_VER 320)
### Fertig implementiert ✅ ### Fertig implementiert ✅
@ -65,32 +65,6 @@ Maps: Leaflet.js + OpenStreetMap (kostenlos, kein Google-Lock)
- Gesundheit, Admin, Karte-Legende: Tab-/Legende-Grid 2 Zeilen (gleiche CSS-Grid-Technik) - Gesundheit, Admin, Karte-Legende: Tab-/Legende-Grid 2 Zeilen (gleiche CSS-Grid-Technik)
- Hinweis: layout.css lädt vor components.css → ID-Selektor (#page-forum, #page-health, #page-admin, #page-map) nötig für Spezifität - Hinweis: layout.css lädt vor components.css → ID-Selektor (#page-forum, #page-health, #page-admin, #page-map) nötig für Spezifität
#### Social Media Manager (2026-04-24)
- Neue Rolle `is_social_media` — eigene Seite `/social`
- Luna KI-Coach: Themen-Vorschläge, Fortschrittsbalken, rotierende Nachrichten
- **Rasse des Tages**: 1003 Wiki-Rassen = 2,75 Jahre täglicher Content mit Bild
- **🎾 Trainingstipp**: 104 Übungen in 7 Kategorien, 3 Stil-Varianten
- **🛁 Pflegetipp**: 43 Tipps rassenspezifisch, auch für normale User im Hundeprofil
- Diversitäts-Check (Warnung wenn Kategorie >40% dominiert)
- Post-Bestätigung mit Datum + URL, Ausstehend-Banner
- Medien-Upload (Kamera/Mediathek/Dateien), Instagram-Vorschau
- XP/Level-System (Rookie → 👑 Star)
- Admin: Social-Tracking (published/scheduled/ideas + letzte 10 Posts)
#### Pflege-System (2026-04-24)
- `pflege_tipps` DB-Tabelle: 43 Tipps in 10 Kategorien (Fell, Krallen, Zähne, Ohren, Augen, Pfoten, Parasiten, Saisonal, Gesundheitsvorsorge, Welpen-Pflege)
- Hundeprofil: 🛁 Pflegetipps — Tipp des Tages (saisonal) + vollständige Kategorieliste
- Rassen-Autocomplete im Hundeprofil mit Wiki-Match-Badge
- `dogs.rasse_id` FK → `wiki_rassen` für präzise Filterung
#### Breed-Enricher Wikipedia-grounded (2026-04-24)
- Korrektheit 2.3→~4.5 durch Wikipedia-Quelltext als Basis
- Claude Haiku extrahiert Fakten aus Wikipedia-Text (de/en Fallback)
- `ki_source` ('wikipedia_de/en'/'none'), `ki_model` getrackt
- LLM-as-Judge Evaluator im Admin, Gemma-Reset-Button
- 1003 Rassen, limit=2000 (ein Rutsch)
- LM Studio: Mac 10.47.11.70:11435, Modell gemma-4-31b-it
#### Infrastruktur #### Infrastruktur
- SSH-Port DS1621: 4711 (geändert von 22, 2026-04-24) - SSH-Port DS1621: 4711 (geändert von 22, 2026-04-24)

View file

@ -121,7 +121,6 @@ from routes.training import router as training_router
from routes.praise import router as praise_router from routes.praise import router as praise_router
from routes.weather import router as weather_router from routes.weather import router as weather_router
from routes.social import router as social_router from routes.social import router as social_router
from routes.moderation import router as moderation_router
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
@ -162,7 +161,6 @@ app.include_router(stats_router, prefix="/api/stats", tags=[
app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"]) app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"])
app.include_router(training_router, prefix="/api/training", tags=["Training"]) app.include_router(training_router, prefix="/api/training", tags=["Training"])
app.include_router(praise_router, prefix="/api/praise", tags=["Praise"]) app.include_router(praise_router, prefix="/api/praise", tags=["Praise"])
app.include_router(moderation_router, prefix="/api/moderation", tags=["Moderation"])
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View file

@ -1,223 +0,0 @@
"""BAN YARO — Moderations-Panel Backend"""
from fastapi import APIRouter, Depends, HTTPException
from database import db
from auth import get_current_user
router = APIRouter()
# ------------------------------------------------------------------
# Dependency: Moderator oder Admin
# ------------------------------------------------------------------
def require_moderator(user=Depends(get_current_user)):
if not (user.get("is_moderator") or user["rolle"] == "admin"):
raise HTTPException(403, "Nur für Moderatoren.")
return user
# ------------------------------------------------------------------
# GET /api/moderation/stats — Übersicht
# ------------------------------------------------------------------
@router.get("/stats")
async def mod_stats(user=Depends(require_moderator)):
with db() as conn:
open_reports = conn.execute(
"SELECT COUNT(*) FROM forum_reports WHERE resolved=0"
).fetchone()[0]
pending_fotos = 0
try:
pending_fotos = conn.execute(
"SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending'"
).fetchone()[0]
except Exception:
pass
banned_users = conn.execute(
"SELECT COUNT(*) FROM users WHERE is_banned=1"
).fetchone()[0]
pending_zuchter = 0
try:
pending_zuchter = conn.execute(
"SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0"
).fetchone()[0]
except Exception:
pass
return {
"open_reports": open_reports,
"pending_fotos": pending_fotos,
"banned_users": banned_users,
"pending_zuchter": pending_zuchter,
}
# ------------------------------------------------------------------
# GET /api/moderation/reports — gemeldete Inhalte
# ------------------------------------------------------------------
@router.get("/reports")
async def mod_reports(user=Depends(require_moderator)):
with db() as conn:
rows = conn.execute("""
SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at,
u.name AS melder_name,
CASE r.target_type
WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id)
WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id)
END AS content_preview
FROM forum_reports r
LEFT JOIN users u ON u.id=r.user_id
WHERE r.resolved=0
ORDER BY r.created_at DESC
LIMIT 100
""").fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# PATCH /api/moderation/reports/{id} — Meldung erledigen
# ------------------------------------------------------------------
@router.patch("/reports/{rid}")
async def mod_resolve_report(rid: int, user=Depends(require_moderator)):
with db() as conn:
r = conn.execute(
"SELECT resolved FROM forum_reports WHERE id=?", (rid,)
).fetchone()
if not r:
raise HTTPException(404, "Meldung nicht gefunden.")
new_state = 0 if r["resolved"] else 1
conn.execute(
"UPDATE forum_reports SET resolved=? WHERE id=?",
(new_state, rid)
)
return {"ok": True}
# ------------------------------------------------------------------
# GET /api/moderation/users — User-Liste (Basisinfos)
# ------------------------------------------------------------------
@router.get("/users")
async def mod_users(
q: str = "",
banned: int = 0,
limit: int = 50,
offset: int = 0,
user=Depends(require_moderator),
):
with db() as conn:
where = "WHERE 1=1"
params = []
if q.strip():
where += " AND (name LIKE ? OR email LIKE ?)"
params.extend([f"%{q.strip()}%", f"%{q.strip()}%"])
if banned:
where += " AND is_banned=1"
# E-Mail nur für Admins; Moderatoren sehen maskierte Version
email_col = "email" if user["rolle"] == "admin" else \
"SUBSTR(email,1,2)||'***@'||SUBSTR(email,INSTR(email,'@')+1) AS email"
rows = conn.execute(f"""
SELECT id, name, {email_col}, rolle, is_moderator, is_banned, ban_reason, created_at
FROM users
{where}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", [*params, limit, offset]).fetchall()
total = conn.execute(
f"SELECT COUNT(*) FROM users {where}", params
).fetchone()[0]
return {"users": [dict(r) for r in rows], "total": total}
# ------------------------------------------------------------------
# PATCH /api/moderation/users/{id} — Ban / Unban
# ------------------------------------------------------------------
@router.patch("/users/{uid}")
async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)):
allowed_fields = {"is_banned", "ban_reason"}
updates = {k: v for k, v in data.items() if k in allowed_fields}
if not updates:
raise HTTPException(400, "Keine erlaubten Felder.")
with db() as conn:
target = conn.execute(
"SELECT id, rolle, name FROM users WHERE id=?", (uid,)
).fetchone()
if not target:
raise HTTPException(404, "User nicht gefunden.")
if target["rolle"] == "admin" and user["rolle"] != "admin":
raise HTTPException(403, "Admins können nur von Admins verwaltet werden.")
cols = ", ".join(f"{k}=?" for k in updates)
conn.execute(
f"UPDATE users SET {cols} WHERE id=?",
[*updates.values(), uid]
)
row = conn.execute(
"SELECT id, name, rolle, is_banned, ban_reason FROM users WHERE id=?",
(uid,)
).fetchone()
return dict(row)
# ------------------------------------------------------------------
# GET /api/moderation/fotos — Wiki-Foto-Einreichungen (pending)
# ------------------------------------------------------------------
@router.get("/fotos")
async def mod_fotos(user=Depends(require_moderator)):
with db() as conn:
try:
rows = conn.execute("""
SELECT s.id, s.rasse_slug, s.foto_url, s.created_at,
u.name AS user_name,
r.name AS rasse_name, r.foto_url AS aktuell_foto
FROM wiki_foto_submissions s
LEFT JOIN users u ON u.id=s.user_id
LEFT JOIN wiki_rassen r ON r.slug=s.rasse_slug
WHERE s.status='pending'
ORDER BY s.created_at ASC
LIMIT 50
""").fetchall()
return [dict(r) for r in rows]
except Exception:
return []
# ------------------------------------------------------------------
# PATCH /api/moderation/fotos/{id} — Foto genehmigen / ablehnen
# ------------------------------------------------------------------
@router.patch("/fotos/{foto_id}")
async def mod_foto_action(foto_id: int, data: dict, user=Depends(require_moderator)):
action = data.get("action")
if action not in ("approve", "reject"):
raise HTTPException(400, "action muss 'approve' oder 'reject' sein.")
with db() as conn:
sub = conn.execute(
"SELECT id, rasse_slug, foto_url FROM wiki_foto_submissions WHERE id=?",
(foto_id,)
).fetchone()
if not sub:
raise HTTPException(404, "Einreichung nicht gefunden.")
if action == "approve":
conn.execute(
"UPDATE wiki_foto_submissions SET status='approved' WHERE id=?",
(foto_id,)
)
conn.execute(
"UPDATE wiki_rassen SET foto_url=? WHERE slug=?",
(sub["foto_url"], sub["rasse_slug"])
)
else:
reason = data.get("reject_reason", "Nicht geeignet.")
conn.execute(
"UPDATE wiki_foto_submissions SET status='rejected', reject_reason=? WHERE id=?",
(reason, foto_id)
)
return {"ok": True}

View file

@ -185,11 +185,6 @@
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Social Media <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Social Media
</div> </div>
<div class="sidebar-item" data-page="moderation" id="sidebar-moderation"
style="display:none;color:var(--c-warning,#f59e0b)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield"></use></svg> Moderation
</div>
<div class="sidebar-item" data-page="admin" id="sidebar-admin" <div class="sidebar-item" data-page="admin" id="sidebar-admin"
style="display:none;color:var(--c-danger,#ef4444)"> style="display:none;color:var(--c-danger,#ef4444)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield"></use></svg> Admin <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield"></use></svg> Admin
@ -343,10 +338,6 @@
<div class="page-body page-container"></div> <div class="page-body page-container"></div>
</section> </section>
<section class="page" id="page-moderation">
<div class="page-body page-container"></div>
</section>
<section class="page" id="page-friends"> <section class="page" id="page-friends">
<div class="page-body page-container"></div> <div class="page-body page-container"></div>
</section> </section>
@ -406,9 +397,9 @@
<div id="modal-container"></div> <div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features --> <!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=94"></script> <script src="/js/api.js?v=93"></script>
<script src="/js/ui.js?v=94"></script> <script src="/js/ui.js?v=93"></script>
<script src="/js/app.js?v=94"></script> <script src="/js/app.js?v=93"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '344'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '343'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => { const App = (() => {
@ -56,7 +56,6 @@ const App = (() => {
chat: { title: 'Nachrichten', module: null, requiresAuth: true }, chat: { title: 'Nachrichten', module: null, requiresAuth: true },
social: { title: 'Social Media', module: null, requiresAuth: true }, social: { title: 'Social Media', module: null, requiresAuth: true },
admin: { title: 'Admin', module: null, requiresAuth: true }, admin: { title: 'Admin', module: null, requiresAuth: true },
moderation: { title: 'Moderation', module: null, requiresAuth: true },
impressum: { title: 'Impressum', module: null }, impressum: { title: 'Impressum', module: null },
datenschutz: { title: 'Datenschutz', module: null }, datenschutz: { title: 'Datenschutz', module: null },
widget: { title: 'Widget', module: null, requiresAuth: true }, widget: { title: 'Widget', module: null, requiresAuth: true },
@ -423,12 +422,6 @@ const App = (() => {
|| state.user.is_moderator; || state.user.is_moderator;
adminItem.style.display = isMod ? '' : 'none'; adminItem.style.display = isMod ? '' : 'none';
} }
const moderationItem = document.getElementById('sidebar-moderation');
if (moderationItem) {
const isMod = state.user.rolle === 'admin' || state.user.rolle === 'moderator'
|| state.user.is_moderator;
moderationItem.style.display = isMod ? '' : 'none';
}
const socialItem = document.getElementById('sidebar-social'); const socialItem = document.getElementById('sidebar-social');
if (socialItem) { if (socialItem) {
const isSocial = state.user.is_social_media || state.user.rolle === 'admin'; const isSocial = state.user.is_social_media || state.user.rolle === 'admin';

View file

@ -1,447 +0,0 @@
/* ============================================================
BAN YARO Moderations-Panel
Nur für Moderatoren und Admins.
============================================================ */
window.Page_moderation = (() => {
let _container = null;
let _appState = null;
let _tab = 'uebersicht';
const TABS = [
{ id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
{ id: 'fotos', label: 'Fotos', icon: 'image' },
{ id: 'user', label: 'User', icon: 'users' },
{ id: 'forum', label: 'Forum', icon: 'chat-circle-dots' },
];
// ------------------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
const u = appState.user;
const isMod = u?.rolle === 'admin' || u?.rolle === 'moderator' || u?.is_moderator;
if (!isMod) {
container.innerHTML = _emptyState('shield', 'Kein Zugriff',
'Dieser Bereich ist nur für Moderatoren und Admins.');
return;
}
_render();
}
function refresh() { _renderTab(); }
function onDogChange() {}
// ------------------------------------------------------------------
// SHELL
// ------------------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="by-tabs adm-tabs" id="mod-tabs">
${TABS.map(t => `
<button class="by-tab${t.id === _tab ? ' active' : ''}" data-tab="${t.id}">
${UI.icon(t.icon)} ${t.label}
</button>
`).join('')}
</div>
<div id="mod-content"></div>
`;
_container.querySelector('#mod-tabs')
?.style.setProperty('--adm-tab-cols', Math.ceil(TABS.length / 2));
_container.querySelectorAll('#mod-tabs .by-tab').forEach(btn => {
btn.addEventListener('click', () => {
_tab = btn.dataset.tab;
_container.querySelectorAll('#mod-tabs .by-tab').forEach(b =>
b.classList.toggle('active', b.dataset.tab === _tab)
);
_renderTab();
});
});
_renderTab();
}
async function _renderTab() {
const el = _container.querySelector('#mod-content');
if (!el) return;
el.innerHTML = `<div style="padding:var(--space-6);text-align:center;
color:var(--c-text-muted)">Lade</div>`;
try {
switch (_tab) {
case 'uebersicht': await _renderStats(el); break;
case 'fotos': await _renderFotos(el); break;
case 'user': await _renderUsers(el); break;
case 'forum': await _renderForum(el); break;
}
} catch (e) {
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
}
}
// ------------------------------------------------------------------
// TAB: ÜBERSICHT
// ------------------------------------------------------------------
async function _renderStats(el) {
const s = await API.get('/moderation/stats');
el.innerHTML = `
<div class="adm-stats-grid">
${_statCard('warning',
'Offene Meldungen',
s.open_reports,
s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')}
${_statCard('image',
'Fotos ausstehend',
s.pending_fotos,
s.pending_fotos > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
${_statCard('skull',
'Gesperrte User',
s.banned_users,
s.banned_users > 0 ? '#f59e0b' : 'var(--c-text-muted)')}
${_statCard('storefront',
'Züchter ausstehend',
s.pending_zuchter,
s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
</div>
<div class="card" style="padding:var(--space-4);margin-top:var(--space-4)">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.6">
${UI.icon('info')}
Das Moderations-Panel zeigt dir alle ausstehenden Aufgaben auf einen Blick.
Verwende die Tabs oben für Details zu Fotos, Usern und Forum-Meldungen.
</p>
</div>
`;
}
function _statCard(icon, label, value, color) {
return `
<div class="card" style="padding:var(--space-4);text-align:center">
<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>
</svg>
<div style="font-size:var(--text-2xl);font-weight:var(--weight-bold);
color:var(--c-text)">${value ?? '—'}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-top:2px">${label}</div>
</div>
`;
}
// ------------------------------------------------------------------
// TAB: FOTOS
// ------------------------------------------------------------------
async function _renderFotos(el) {
el.innerHTML = `
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
<button class="btn btn-ghost btn-sm" id="mod-fotos-refresh">
${UI.icon('arrows-clockwise')} Aktualisieren
</button>
</div>
<div id="mod-fotos-list">Lade</div>
`;
el.querySelector('#mod-fotos-refresh').addEventListener('click', () =>
_loadFotos(el.querySelector('#mod-fotos-list'))
);
await _loadFotos(el.querySelector('#mod-fotos-list'));
}
async function _loadFotos(el) {
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;
color:var(--c-text-muted)">Lade</div>`;
const fotos = await API.get('/moderation/fotos');
if (!fotos.length) {
el.innerHTML = _emptyState('check-circle', 'Keine ausstehenden Fotos',
'Alle Foto-Einreichungen wurden bearbeitet.');
return;
}
el.innerHTML = `
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
gap:var(--space-4)">
${fotos.map(f => `
<div class="card" style="padding:var(--space-4)">
<img src="${_esc(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 || f.rasse_slug)}
</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
margin-bottom:var(--space-3)">
von ${_esc(f.user_name)}
</div>
${f.aktuell_foto ? `
<img src="${_esc(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>
` : ''}
<div style="display:flex;gap:var(--space-2)">
<button class="btn btn-sm btn-primary mod-foto-approve"
data-id="${f.id}" style="flex:1">Freigeben</button>
<button class="btn btn-sm btn-ghost mod-foto-reject"
data-id="${f.id}" style="color:var(--c-danger)">Ablehnen</button>
</div>
</div>
`).join('')}
</div>
`;
el.querySelectorAll('.mod-foto-approve').forEach(btn => {
btn.addEventListener('click', async () => {
btn.disabled = true;
try {
await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'approve' });
UI.toast.success('Foto freigegeben.');
await _loadFotos(el);
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
});
});
el.querySelectorAll('.mod-foto-reject').forEach(btn => {
btn.addEventListener('click', async () => {
btn.disabled = true;
try {
await API.patch(`/moderation/fotos/${btn.dataset.id}`, {
action: 'reject',
reject_reason: 'Nicht geeignet.'
});
UI.toast.success('Foto abgelehnt.');
await _loadFotos(el);
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
});
});
}
// ------------------------------------------------------------------
// TAB: USER
// ------------------------------------------------------------------
async function _renderUsers(el) {
el.innerHTML = `
<div class="adm-filter-row">
<input id="mod-user-q" type="search" placeholder="Name oder E-Mail…"
class="adm-filter-input">
<label style="display:flex;align-items:center;gap:var(--space-2);
font-size:var(--text-sm);color:var(--c-text-secondary)">
<input type="checkbox" id="mod-only-banned"> Nur gesperrte
</label>
</div>
<div id="mod-user-list">Lade</div>
`;
const load = async () => {
const q = el.querySelector('#mod-user-q').value;
const banned = el.querySelector('#mod-only-banned').checked ? 1 : 0;
const data = await API.get(
`/moderation/users?q=${encodeURIComponent(q)}&banned=${banned}`
);
_renderUserList(el.querySelector('#mod-user-list'), data.users, data.total, el);
};
let timer;
el.querySelector('#mod-user-q').addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(load, 350);
});
el.querySelector('#mod-only-banned').addEventListener('change', load);
await load();
}
function _renderUserList(el, users, total, parentEl) {
if (!users.length) {
el.innerHTML = _emptyState('users', 'Keine Nutzer gefunden', '');
return;
}
el.innerHTML = `
<div style="margin-bottom:var(--space-2);font-size:var(--text-xs);
color:var(--c-text-muted)">${total} Nutzer gefunden</div>
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
${users.map(u => `
<div class="card" style="padding:var(--space-3) var(--space-4);
${u.is_banned ? 'opacity:0.6;border-left:3px solid var(--c-danger)' : ''}">
<div style="display:flex;align-items:center;gap:var(--space-3)">
<div style="width:36px;height:36px;border-radius:50%;flex-shrink:0;
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())}
</div>
<div style="flex:1;min-width:0">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">
${_esc(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 style="font-size:var(--text-xs);color:var(--c-text-muted)">
${_esc(u.email)} ·
<span style="color:${
u.rolle === 'admin' ? 'var(--c-danger)'
: u.rolle === 'moderator' ? '#f59e0b'
: 'var(--c-text-muted)'}">
${_esc(u.rolle)}
</span>
</div>
</div>
<div style="flex-shrink:0">
${u.is_banned
? `<button class="btn btn-sm btn-ghost mod-unban"
data-uid="${u.id}" data-name="${_esc(u.name)}"
title="Sperre aufheben" style="color:var(--c-success)">
${UI.icon('lock-open')}
</button>`
: `<button class="btn btn-sm btn-ghost mod-ban"
data-uid="${u.id}" data-name="${_esc(u.name)}"
title="Sperren" style="color:var(--c-danger)">
${UI.icon('lock')}
</button>`
}
</div>
</div>
</div>
`).join('')}
</div>
`;
el.querySelectorAll('.mod-ban').forEach(btn => {
btn.addEventListener('click', () => _banUser(btn.dataset.uid, btn.dataset.name, true, parentEl));
});
el.querySelectorAll('.mod-unban').forEach(btn => {
btn.addEventListener('click', () => _banUser(btn.dataset.uid, btn.dataset.name, false, parentEl));
});
}
async function _banUser(uid, name, ban, parentEl) {
if (ban) {
const reason = window.prompt(`${name} sperren — Grund (optional):`);
if (reason === null) return;
try {
await API.patch(`/moderation/users/${uid}`, {
is_banned: 1,
ban_reason: reason || 'Kein Grund angegeben.'
});
UI.toast.success(`${name} gesperrt.`);
_renderTab();
} catch (e) { UI.toast.error(e.message); }
} else {
try {
await API.patch(`/moderation/users/${uid}`, {
is_banned: 0,
ban_reason: null
});
UI.toast.success(`Sperre für ${name} aufgehoben.`);
_renderTab();
} catch (e) { UI.toast.error(e.message); }
}
}
// ------------------------------------------------------------------
// TAB: FORUM
// ------------------------------------------------------------------
async function _renderForum(el) {
el.innerHTML = `
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
<button class="btn btn-ghost btn-sm" id="mod-forum-refresh">
${UI.icon('arrows-clockwise')} Aktualisieren
</button>
</div>
<div id="mod-forum-list">Lade</div>
`;
el.querySelector('#mod-forum-refresh').addEventListener('click', () =>
_loadReports(el.querySelector('#mod-forum-list'))
);
await _loadReports(el.querySelector('#mod-forum-list'));
}
async function _loadReports(el) {
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;
color:var(--c-text-muted)">Lade</div>`;
const reports = await API.get('/moderation/reports');
if (!reports.length) {
el.innerHTML = _emptyState('check-circle', 'Keine offenen Meldungen', 'Alles sauber.');
return;
}
el.innerHTML = `
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
${reports.map(r => `
<div class="card" style="padding:var(--space-4);
border-left:3px solid var(--c-danger)">
<div style="display:flex;align-items:flex-start;gap:var(--space-3)">
<div style="flex:1;min-width:0">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
margin-bottom:var(--space-1)">
${_esc(r.target_type)} #${r.target_id} ·
Gemeldet von <strong>${_esc(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)}
</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>` : ''}
</div>
<button class="btn btn-sm btn-primary mod-resolve-btn"
data-rid="${r.id}" title="Als erledigt markieren">
${UI.icon('check')}
</button>
</div>
</div>
`).join('')}
</div>
`;
el.querySelectorAll('.mod-resolve-btn').forEach(btn => {
btn.addEventListener('click', async () => {
btn.disabled = true;
try {
await API.patch(`/moderation/reports/${btn.dataset.rid}`, {});
UI.toast.success('Meldung als erledigt markiert.');
await _loadReports(el);
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
});
});
}
// ------------------------------------------------------------------
// HELPERS
// ------------------------------------------------------------------
function _emptyState(icon, title, text) {
return `
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
<svg class="ph-icon" style="width:40px;height:40px;color:var(--c-border);
margin-bottom:var(--space-3)" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
<p style="font-weight:var(--weight-semibold);color:var(--c-text);
margin:0 0 var(--space-1)">${title}</p>
${text ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0">${text}</p>` : ''}
</div>
`;
}
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ------------------------------------------------------------------
return { init, refresh, onDogChange };
})();

View file

@ -43,11 +43,9 @@ window.Page_social = (() => {
function _render() { function _render() {
const lvlBar = _stats ? _levelBar(_stats) : ''; const lvlBar = _stats ? _levelBar(_stats) : '';
_el.innerHTML = ` _el.innerHTML = `
<div style="background:var(--c-surface);border-radius:var(--radius-lg); <div style="margin-bottom:var(--space-3)">
box-shadow:var(--shadow-sm);padding:var(--space-4); <div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)">
margin-bottom:var(--space-4)"> <span style="font-size:1.5em">📱</span>
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
<span style="font-size:1.6em">📱</span>
<div style="flex:1;min-width:0"> <div style="flex:1;min-width:0">
<div style="font-size:var(--text-base);font-weight:700">Social Media</div> <div style="font-size:var(--text-base);font-weight:700">Social Media</div>
<div style="font-size:11px;color:var(--c-text-muted)">Luna ist dein KI-Coach</div> <div style="font-size:11px;color:var(--c-text-muted)">Luna ist dein KI-Coach</div>
@ -62,7 +60,7 @@ window.Page_social = (() => {
<!-- Tabs --> <!-- Tabs -->
<div style="display:flex;border-bottom:2px solid var(--c-border); <div style="display:flex;border-bottom:2px solid var(--c-border);
margin-bottom:var(--space-4)"> margin-bottom:var(--space-3)">
${[['idee','✨ Ideen'],['archiv','📂 Archiv'],['bewerten','🔍 Prüfen']].map(([t,l]) => ` ${[['idee','✨ Ideen'],['archiv','📂 Archiv'],['bewerten','🔍 Prüfen']].map(([t,l]) => `
<button class="sm-tab" data-tab="${t}" style="flex:1;padding:10px 4px;border:none; <button class="sm-tab" data-tab="${t}" style="flex:1;padding:10px 4px;border:none;
background:none;cursor:pointer;font-size:13px;white-space:nowrap; background:none;cursor:pointer;font-size:13px;white-space:nowrap;
@ -88,13 +86,13 @@ window.Page_social = (() => {
((s.xp - s.xp_current_min) / (s.xp_next - s.xp_current_min)) * 100)); ((s.xp - s.xp_current_min) / (s.xp_next - s.xp_current_min)) * 100));
return `<div> return `<div>
<div style="display:flex;justify-content:space-between;font-size:12px; <div style="display:flex;justify-content:space-between;font-size:12px;
color:var(--c-text);margin-bottom:6px;font-weight:500"> color:var(--c-text);margin-bottom:4px;font-weight:500">
<span>${s.level}</span> <span>${s.level}</span>
${s.next_level ? `<span style="color:var(--c-text-secondary)">${s.xp_next - s.xp} XP bis ${s.next_level}</span>` : ''} ${s.next_level ? `<span style="color:var(--c-text-secondary)">${s.xp_next - s.xp} XP bis ${s.next_level}</span>` : ''}
</div> </div>
<div style="background:var(--c-surface-2);border-radius:var(--radius-full);height:8px;overflow:hidden"> <div style="background:var(--c-border);border-radius:4px;height:6px;overflow:hidden">
<div style="height:100%;background:linear-gradient(90deg,var(--c-primary),var(--c-primary-light)); <div style="height:100%;background:var(--c-primary);width:${pct}%;
width:${pct}%;border-radius:var(--radius-full);transition:width .5s"></div> border-radius:4px;transition:width .5s"></div>
</div> </div>
</div>`; </div>`;
} }
@ -107,15 +105,14 @@ window.Page_social = (() => {
el.innerHTML = ` el.innerHTML = `
<!-- Luna --> <!-- Luna -->
<div style="background:var(--c-surface);border-radius:var(--radius-lg); <div style="background:var(--c-surface-2);
box-shadow:var(--shadow-sm);padding:var(--space-4); border-radius:12px;padding:12px;margin-bottom:var(--space-3);
margin-bottom:var(--space-4); display:flex;gap:10px;align-items:flex-start">
display:flex;gap:var(--space-3);align-items:flex-start"> <span style="font-size:1.8em;flex-shrink:0">🌙</span>
<span style="font-size:2em;flex-shrink:0">🌙</span>
<div> <div>
<div style="font-weight:700;font-size:var(--text-sm);margin-bottom:4px"> <div style="font-weight:700;font-size:var(--text-sm);margin-bottom:2px">
Hey, ich bin Luna 👋</div> Hey, ich bin Luna 👋</div>
<div style="font-size:13px;color:var(--c-text-secondary);line-height:1.6"> <div style="font-size:13px;color:var(--c-text);line-height:1.5">
Ich schlage dir Ideen vor und erkläre warum sie funktionieren. Ich schlage dir Ideen vor und erkläre warum sie funktionieren.
Lern Social Media richtig nicht nur kopieren!</div> Lern Social Media richtig nicht nur kopieren!</div>
</div> </div>
@ -123,9 +120,9 @@ window.Page_social = (() => {
<!-- Diversitäts-Warnung --> <!-- Diversitäts-Warnung -->
${_diversity?.warning ? ` ${_diversity?.warning ? `
<div style="background:var(--c-warning-subtle);border:1.5px solid var(--c-warning); <div style="background:var(--c-surface-2);border:2px solid var(--c-warning);
border-radius:var(--radius-lg);padding:var(--space-4);margin-bottom:var(--space-4)"> border-radius:12px;padding:12px;margin-bottom:var(--space-3)">
<div style="display:flex;gap:var(--space-3);align-items:flex-start"> <div style="display:flex;gap:8px;align-items:flex-start">
<span style="font-size:1.3em;flex-shrink:0"></span> <span style="font-size:1.3em;flex-shrink:0"></span>
<div> <div>
<div style="font-weight:700;font-size:var(--text-sm);color:var(--c-warning); <div style="font-weight:700;font-size:var(--text-sm);color:var(--c-warning);
@ -142,74 +139,64 @@ window.Page_social = (() => {
</div>` : ''} </div>` : ''}
<!-- Vorschläge --> <!-- Vorschläge -->
<div style="background:var(--c-surface);border-radius:var(--radius-lg); <div style="margin-bottom:var(--space-4)">
box-shadow:var(--shadow-sm);padding:var(--space-4);
margin-bottom:var(--space-4)">
<div style="display:flex;align-items:center;justify-content:space-between; <div style="display:flex;align-items:center;justify-content:space-between;
margin-bottom:var(--space-3)"> margin-bottom:var(--space-2)">
<div style="font-weight:700;font-size:var(--text-sm)">💡 Was könntest du heute posten?</div> <div style="font-weight:600;font-size:var(--text-sm)">💡 Was könntest du heute posten?</div>
<button id="sm-refresh" class="btn btn-sm btn-secondary" <button id="sm-refresh" class="btn btn-sm btn-secondary"
style="font-size:11px;padding:4px 12px;min-height:32px; style="font-size:11px;padding:3px 10px;min-height:30px"> Neue</button>
border-radius:var(--radius-full)"> Neue</button>
</div> </div>
<div id="sm-suggestions"> <div id="sm-suggestions">
${_lunaThinking('Klein...')} ${_lunaThinking('Klein...')}
</div> </div>
</div> </div>
<div style="display:flex;align-items:center;gap:var(--space-3); <div style="border-top:1px solid var(--c-border);padding-top:var(--space-3);
margin-bottom:var(--space-3)"> margin-bottom:var(--space-2)">
<div style="flex:1;height:1px;background:var(--c-border)"></div> <div style="font-weight:600;font-size:var(--text-sm)"> Oder eigenes Thema</div>
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text-muted);
white-space:nowrap"> Oder eigenes Thema</div>
<div style="flex:1;height:1px;background:var(--c-border)"></div>
</div> </div>
<!-- Formular --> <!-- Formular -->
<div style="background:var(--c-surface);border-radius:var(--radius-lg); <div class="card" style="padding:var(--space-3)">
box-shadow:var(--shadow-sm);padding:var(--space-4)">
<!-- Plattform --> <!-- Plattform -->
<div style="margin-bottom:var(--space-4)"> <div style="margin-bottom:var(--space-3)">
<div class="sm-label">Plattform</div> <div class="sm-label">Plattform</div>
<div style="display:flex;gap:var(--space-2)"> <div style="display:flex;gap:var(--space-2)">
${['both','instagram','tiktok'].map((p,i) => ` ${['both','instagram','tiktok'].map((p,i) => `
<button class="btn btn-sm sm-plat ${i===0?'btn-primary':'btn-secondary'}" <button class="btn btn-sm sm-plat ${i===0?'btn-primary':'btn-secondary'}"
data-p="${p}" style="flex:1;min-height:36px;font-size:12px;padding:4px 8px; data-p="${p}" style="flex:1;min-height:38px;font-size:12px;padding:4px">
border-radius:var(--radius-full)">
${_PL[p]}</button>`).join('')} ${_PL[p]}</button>`).join('')}
</div> </div>
</div> </div>
<!-- Format --> <!-- Format -->
<div style="margin-bottom:var(--space-4)"> <div style="margin-bottom:var(--space-3)">
<div class="sm-label">Format</div> <div class="sm-label">Format</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
${['post','reel','story','carousel'].map((f,i) => ` ${['post','reel','story','carousel'].map((f,i) => `
<button class="btn btn-sm sm-fmt ${i===0?'btn-primary':'btn-secondary'}" <button class="btn btn-sm sm-fmt ${i===0?'btn-primary':'btn-secondary'}"
data-f="${f}" style="min-height:36px;font-size:12px;padding:4px 8px; data-f="${f}" style="min-height:38px;font-size:12px;padding:4px">
border-radius:var(--radius-full)">
${_FL[f]}</button>`).join('')} ${_FL[f]}</button>`).join('')}
</div> </div>
</div> </div>
<!-- Thema --> <!-- Thema -->
<div style="margin-bottom:var(--space-4)"> <div style="margin-bottom:var(--space-3)">
<div class="sm-label">Thema</div> <div class="sm-label">Thema</div>
<textarea id="sm-topic" rows="3" <textarea id="sm-topic" rows="3"
placeholder="z.B. Mein Hund beim ersten Schnee 🐾" placeholder="z.B. Mein Hund beim ersten Schnee 🐾"
style="width:100%;font-size:var(--text-sm);resize:none;line-height:1.5; style="width:100%;font-size:var(--text-sm);resize:none;line-height:1.5;
background:var(--c-surface-2);color:var(--c-text); background:var(--c-surface-2);color:var(--c-text);
border:1.5px solid var(--c-border);border-radius:var(--radius-md); border:1.5px solid var(--c-border);border-radius:8px;
padding:10px 12px;box-sizing:border-box; padding:10px 12px;box-sizing:border-box;
font-family:inherit"></textarea> font-family:inherit"></textarea>
</div> </div>
<!-- "Was jetzt?"-Banner --> <!-- "Was jetzt?"-Banner -->
<div id="sm-next-hint" style="display:none;background:var(--c-primary); <div id="sm-next-hint" style="display:none;background:var(--c-primary);
color:#fff;border-radius:var(--radius-md);padding:10px 14px; color:#fff;border-radius:8px;padding:10px 12px;margin-bottom:var(--space-3);
margin-bottom:var(--space-4);
font-size:var(--text-sm);font-weight:600;text-align:center"> font-size:var(--text-sm);font-weight:600;text-align:center">
Idee übernommen prüf die Einstellungen und tippe auf <strong>Los geht's!</strong> 👇 Idee übernommen prüf die Einstellungen und tippe auf <strong>Los geht's!</strong> 👇
</div> </div>
<!-- Medien-Upload --> <!-- Medien-Upload -->
<div style="margin-bottom:var(--space-4)"> <div style="margin-bottom:var(--space-3)">
<div class="sm-label">Foto / Video (optional)</div> <div class="sm-label">Foto / Video (optional)</div>
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap"> <div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
<label style="cursor:pointer;flex:1"> <label style="cursor:pointer;flex:1">
@ -217,7 +204,7 @@ window.Page_social = (() => {
capture="environment" style="display:none"> capture="environment" style="display:none">
<span class="btn btn-secondary btn-sm" <span class="btn btn-secondary btn-sm"
style="min-height:40px;display:flex;align-items:center;justify-content:center; style="min-height:40px;display:flex;align-items:center;justify-content:center;
gap:6px;font-size:12px;width:100%;border-radius:var(--radius-full)"> gap:6px;font-size:12px;width:100%">
📷 Kamera / Mediathek</span> 📷 Kamera / Mediathek</span>
</label> </label>
<label style="cursor:pointer;flex:1"> <label style="cursor:pointer;flex:1">
@ -225,35 +212,32 @@ window.Page_social = (() => {
style="display:none"> style="display:none">
<span class="btn btn-secondary btn-sm" <span class="btn btn-secondary btn-sm"
style="min-height:40px;display:flex;align-items:center;justify-content:center; style="min-height:40px;display:flex;align-items:center;justify-content:center;
gap:6px;font-size:12px;width:100%;border-radius:var(--radius-full)"> gap:6px;font-size:12px;width:100%">
📁 Dateien</span> 📁 Dateien</span>
</label> </label>
</div> </div>
<div id="sm-media-preview" style="display:none;margin-top:8px; <div id="sm-media-preview" style="display:none;margin-top:8px;
max-width:100px;max-height:100px;border-radius:var(--radius-md);overflow:hidden"></div> max-width:100px;max-height:100px;border-radius:8px;overflow:hidden"></div>
</div> </div>
<!-- Rasse Luna-Vorschlag + Suche --> <!-- Rasse Luna-Vorschlag + Suche -->
<div style="margin-bottom:var(--space-4)"> <div style="margin-bottom:var(--space-3)">
<div class="sm-label">Rasse (optional)</div> <div class="sm-label">Rasse (optional)</div>
${_unusedBreeds.length ? ` ${_unusedBreeds.length ? `
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:8px"> <div style="font-size:11px;color:var(--c-text-muted);margin-bottom:6px">
🌙 Noch nicht gezeigt: 🌙 Noch nicht gezeigt:
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
${_unusedBreeds.map(b => ${_unusedBreeds.map(b =>
`<button class="sm-breed-chip" data-id="${b.id}" data-name="${_esc(b.name)}" `<button class="sm-breed-chip" data-id="${b.id}" data-name="${_esc(b.name)}"
style="padding:5px 12px;border-radius:var(--radius-full); style="display:inline-block;margin:2px 4px 2px 0;padding:3px 10px;
border:1.5px solid var(--c-border); border-radius:20px;border:1.5px solid var(--c-border);
background:var(--c-surface-2);color:var(--c-text); background:var(--c-surface-2);color:var(--c-text);
font-size:12px;cursor:pointer;font-family:inherit; font-size:11px;cursor:pointer;font-family:inherit">
transition:all var(--transition-fast)">
${_esc(b.name)}</button>`).join('')} ${_esc(b.name)}</button>`).join('')}
</div>
</div>` : ''} </div>` : ''}
<input id="sm-breed-search" list="sm-breed-list" <input id="sm-breed-search" list="sm-breed-list"
placeholder="Rasse suchen oder leer lassen…" placeholder="Rasse suchen oder leer lassen…"
autocomplete="off" autocomplete="off"
style="width:100%;background:var(--c-surface-2);color:var(--c-text); style="width:100%;background:var(--c-surface-2);color:var(--c-text);
border:1.5px solid var(--c-border);border-radius:var(--radius-md); border:1.5px solid var(--c-border);border-radius:8px;
padding:9px 12px;font-size:var(--text-sm); padding:9px 12px;font-size:var(--text-sm);
font-family:inherit;box-sizing:border-box"> font-family:inherit;box-sizing:border-box">
<datalist id="sm-breed-list"> <datalist id="sm-breed-list">
@ -261,63 +245,35 @@ window.Page_social = (() => {
</datalist> </datalist>
<input type="hidden" id="sm-breed-id"> <input type="hidden" id="sm-breed-id">
</div> </div>
<button id="sm-training-tip" class="btn btn-secondary"
<!-- Generier-Buttons als Cards --> style="width:100%;min-height:44px;font-size:var(--text-sm);
<div class="sm-label" style="margin-bottom:var(--space-3)">Schnell generieren</div> margin-bottom:4px;border:1.5px solid #10b981;color:#10b981">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:var(--space-2); 🎾 Trainingstipp generieren
margin-bottom:var(--space-3)"> <span style="font-size:10px;opacity:.7;margin-left:6px">104 Übungen</span>
<button id="sm-breed-day" </button>
style="display:flex;flex-direction:column;align-items:center; <button id="sm-pflege-tip" class="btn btn-secondary"
gap:6px;padding:var(--space-3) var(--space-2); style="width:100%;min-height:44px;font-size:var(--text-sm);
background:var(--c-primary-subtle);border:1.5px solid var(--c-primary-light); margin-bottom:4px;border:1.5px solid #a78bfa;color:#a78bfa">
border-radius:var(--radius-lg);cursor:pointer; 🛁 Pflegetipp generieren
font-family:inherit;transition:all var(--transition-fast); <span style="font-size:10px;opacity:.7;margin-left:6px">allg. oder für gewählte Rasse</span>
box-shadow:var(--shadow-xs)"> </button>
<span style="font-size:1.8em">🐾</span> <button id="sm-show-exercises" class="btn btn-secondary"
<span style="font-size:11px;font-weight:600;color:var(--c-primary-dark); style="width:100%;min-height:36px;font-size:11px;
text-align:center;line-height:1.3">Rasse des Tages</span> margin-bottom:8px;color:var(--c-text-muted)">
${_unusedBreeds.length ? `<span style="font-size:10px;color:var(--c-text-muted)">${_unusedBreeds.length} übrig</span>` : ''} 📋 Alle Übungen ansehen
</button> </button>
<button id="sm-training-tip" <button id="sm-breed-day" class="btn btn-secondary"
style="display:flex;flex-direction:column;align-items:center; style="width:100%;min-height:44px;font-size:var(--text-sm);
gap:6px;padding:var(--space-3) var(--space-2); margin-bottom:8px;border:1.5px solid var(--c-primary);
background:#f0fdf4;border:1.5px solid #86efac; color:var(--c-primary)">
border-radius:var(--radius-lg);cursor:pointer; 🐾 Rasse des Tages generieren
font-family:inherit;transition:all var(--transition-fast); ${_unusedBreeds.length ? `<span style="font-size:10px;opacity:.7;margin-left:6px">(${_breeds.length - (_breeds.length - _unusedBreeds.length)} noch übrig)</span>` : ''}
box-shadow:var(--shadow-xs)">
<span style="font-size:1.8em">🎾</span>
<span style="font-size:11px;font-weight:600;color:#15803d;
text-align:center;line-height:1.3">Trainingstipp</span>
<span style="font-size:10px;color:#4ade80">104 Übungen</span>
</button>
<button id="sm-pflege-tip"
style="display:flex;flex-direction:column;align-items:center;
gap:6px;padding:var(--space-3) var(--space-2);
background:#faf5ff;border:1.5px solid #d8b4fe;
border-radius:var(--radius-lg);cursor:pointer;
font-family:inherit;transition:all var(--transition-fast);
box-shadow:var(--shadow-xs)">
<span style="font-size:1.8em">🛁</span>
<span style="font-size:11px;font-weight:600;color:#7c3aed;
text-align:center;line-height:1.3">Pflegetipp</span>
<span style="font-size:10px;color:#c084fc">je Rasse</span>
</button>
</div>
<button id="sm-show-exercises"
style="width:100%;min-height:36px;font-size:11px;cursor:pointer;
margin-bottom:var(--space-4);color:var(--c-text-muted);
background:none;border:1px dashed var(--c-border);
border-radius:var(--radius-md);font-family:inherit;
transition:all var(--transition-fast)">
📋 Alle 104 Übungen ansehen
</button> </button>
<button id="sm-gen" class="btn btn-primary" <button id="sm-gen" class="btn btn-primary"
style="width:100%;min-height:52px;font-size:var(--text-base); style="width:100%;min-height:48px;font-size:var(--text-base)">
border-radius:var(--radius-lg);box-shadow:var(--shadow-md)">
Los geht's! Los geht's!
</button> </button>
<div id="sm-gen-result" style="margin-top:var(--space-4)"></div> <div id="sm-gen-result" style="margin-top:var(--space-3)"></div>
</div>`; </div>`;
// Platform toggle // Platform toggle
@ -541,15 +497,13 @@ window.Page_social = (() => {
await new Promise(r => setTimeout(r, 400)); await new Promise(r => setTimeout(r, 400));
const stilLabel = {tutorial:'📹 Tutorial', community:'🙋 Community', aspirational:'💪 Aspirational'}[data.stil] || ''; const stilLabel = {tutorial:'📹 Tutorial', community:'🙋 Community', aspirational:'💪 Aspirational'}[data.stil] || '';
res.innerHTML = ` res.innerHTML = `
<div style="background:#f0fdf4;border:1px solid #86efac; <div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
border-radius:var(--radius-lg);padding:var(--space-4); margin-bottom:10px;display:flex;gap:10px;align-items:center">
margin-bottom:var(--space-3);display:flex;gap:var(--space-3);align-items:center; <span style="font-size:2.5em;flex-shrink:0">🎾</span>
box-shadow:var(--shadow-xs)">
<span style="font-size:2.2em;flex-shrink:0">🎾</span>
<div> <div>
<div style="font-size:11px;color:#4ade80;font-weight:600;margin-bottom:2px"> <div style="font-size:11px;color:var(--c-text-muted)">
Trainingstipp · ${_esc(data.exercise_kat||'')} · ${stilLabel}</div> Trainingstipp · ${_esc(data.exercise_kat||'')} · ${stilLabel}</div>
<div style="font-weight:700;font-size:var(--text-base);color:#15803d"> <div style="font-weight:700;font-size:var(--text-base)">
${_esc(data.exercise_name||'')}</div> ${_esc(data.exercise_name||'')}</div>
</div> </div>
</div> </div>
@ -581,16 +535,14 @@ window.Page_social = (() => {
_progressDone(res); _progressDone(res);
await new Promise(r => setTimeout(r, 400)); await new Promise(r => setTimeout(r, 400));
res.innerHTML = ` res.innerHTML = `
<div style="background:#faf5ff;border:1px solid #d8b4fe; <div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
border-radius:var(--radius-lg);padding:var(--space-4); margin-bottom:10px;display:flex;gap:10px;align-items:center">
margin-bottom:var(--space-3);display:flex;gap:var(--space-3);align-items:center; <span style="font-size:2em;flex-shrink:0">🛁</span>
box-shadow:var(--shadow-xs)">
<span style="font-size:2.2em;flex-shrink:0">🛁</span>
<div> <div>
<div style="font-size:11px;color:#c084fc;font-weight:600;margin-bottom:2px"> <div style="font-size:11px;color:var(--c-text-muted)">
Pflegetipp · ${_esc(data.pflege_kat||'')} Pflegetipp · ${_esc(data.pflege_kat||'')}
${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div> ${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div>
<div style="font-weight:700;font-size:var(--text-base);color:#7c3aed"> <div style="font-weight:700;font-size:var(--text-base)">
${_esc(data.pflege_titel||'')}</div> ${_esc(data.pflege_titel||'')}</div>
</div> </div>
</div> </div>
@ -620,18 +572,14 @@ window.Page_social = (() => {
// Foto anzeigen wenn vorhanden // Foto anzeigen wenn vorhanden
const mediaUrl = data.breed_foto || data.media_url || null; const mediaUrl = data.breed_foto || data.media_url || null;
res.innerHTML = ` res.innerHTML = `
<div style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light); <div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
border-radius:var(--radius-lg);padding:var(--space-4); margin-bottom:10px;display:flex;gap:10px;align-items:center">
margin-bottom:var(--space-3);display:flex;gap:var(--space-3);align-items:center;
box-shadow:var(--shadow-xs)">
${mediaUrl ? `<img src="${mediaUrl}" ${mediaUrl ? `<img src="${mediaUrl}"
style="width:60px;height:60px;border-radius:var(--radius-md);object-fit:cover;flex-shrink:0" style="width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0"
onerror="this.style.display='none'">` : '<span style="font-size:2.2em">🐶</span>'} onerror="this.style.display='none'">` : '<span style="font-size:2.5em">🐶</span>'}
<div> <div>
<div style="font-size:11px;color:var(--c-primary);font-weight:600;margin-bottom:2px"> <div style="font-size:11px;color:var(--c-text-muted)">Rasse des Tages</div>
Rasse des Tages</div> <div style="font-weight:700;font-size:var(--text-base)">${_esc(data.topic?.replace('Rasse des Tages: ',''))}</div>
<div style="font-weight:700;font-size:var(--text-base);color:var(--c-primary-dark)">
${_esc(data.topic?.replace('Rasse des Tages: ',''))}</div>
</div> </div>
</div> </div>
${_renderResult(data, mediaUrl)}`; ${_renderResult(data, mediaUrl)}`;
@ -776,43 +724,33 @@ window.Page_social = (() => {
return ` return `
${data.coaching ? ` ${data.coaching ? `
<div style="background:var(--c-primary-subtle); <div style="background:var(--c-surface-2);
border-radius:var(--radius-lg);padding:var(--space-4);margin-bottom:var(--space-3); border-radius:12px;padding:12px;margin-bottom:12px;
border-left:4px solid var(--c-primary)"> border-left:3px solid var(--c-primary)">
<div style="display:flex;gap:var(--space-3)"> <div style="display:flex;gap:8px">
<span style="font-size:1.3em;flex-shrink:0">🌙</span> <span style="font-size:1.2em;flex-shrink:0">🌙</span>
<div> <div>
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px; <div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:3px">
text-transform:uppercase;letter-spacing:.5px">Luna sagt:</div> Luna sagt:</div>
<div style="font-size:var(--text-sm);line-height:1.6;color:var(--c-text)">${_esc(data.coaching)}</div> <div style="font-size:var(--text-sm);line-height:1.5;color:var(--c-text)">${_esc(data.coaching)}</div>
</div> </div>
</div> </div>
</div>` : ''} </div>` : ''}
<div style="display:flex;align-items:center;gap:var(--space-2); <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
margin-bottom:var(--space-3);flex-wrap:wrap"> <span style="background:#f0fdf4;color:var(--c-success);border-radius:8px;
<span style="background:var(--c-success-subtle);color:var(--c-success); padding:4px 10px;font-size:11px;font-weight:600"> Gespeichert</span>
border-radius:var(--radius-full); ${score ? `<span>${score}</span>` : ''}
padding:4px 12px;font-size:11px;font-weight:700"> Gespeichert</span> <button class="btn btn-sm btn-secondary sm-preview-btn"
${score ? `<span style="font-size:13px">${score}</span>` : ''}
</div>
<!-- Aktions-Buttons: Primär volle Breite, Sekundär nebeneinander -->
<div style="margin-bottom:var(--space-4)">
<button class="btn btn-primary sm-posted-btn"
data-id="${data.id}" data-id="${data.id}"
style="width:100%;min-height:48px;font-size:var(--text-sm); style="font-size:11px;padding:4px 10px;min-height:30px">
margin-bottom:var(--space-2);border-radius:var(--radius-lg); 👁 Vorschau</button>
background:#10b981;border-color:#10b981;box-shadow:var(--shadow-sm)"> <button class="btn btn-sm btn-primary sm-posted-btn"
data-id="${data.id}"
style="margin-left:auto;font-size:11px;padding:4px 12px;min-height:30px;
background:#10b981;border-color:#10b981">
📤 Habe ich gepostet! 📤 Habe ich gepostet!
</button> </button>
<div style="display:flex;gap:var(--space-2)">
<button class="btn btn-sm btn-secondary sm-preview-btn"
data-id="${data.id}"
style="flex:1;font-size:12px;padding:6px 10px;min-height:36px;
border-radius:var(--radius-full)">
👁 Vorschau</button>
</div>
</div> </div>
<div id="sm-posted-form-${data.id}" style="display:none;background:var(--c-surface-2); <div id="sm-posted-form-${data.id}" style="display:none;background:var(--c-surface-2);
border-radius:10px;padding:12px;margin-bottom:12px"> border-radius:10px;padding:12px;margin-bottom:12px">
@ -844,62 +782,49 @@ window.Page_social = (() => {
</div> </div>
${mediaUrl ? ` ${mediaUrl ? `
<div style="background:var(--c-surface);border:1px solid var(--c-border); <div class="card" style="padding:12px;margin-bottom:10px">
border-radius:var(--radius-lg);padding:var(--space-4);
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">📎 Dein Medien-Upload</div> <div class="sm-label">📎 Dein Medien-Upload</div>
<img src="${mediaUrl}" style="max-width:100%;max-height:200px; <img src="${mediaUrl}" style="max-width:100%;max-height:200px;border-radius:8px;
border-radius:var(--radius-md);object-fit:cover;margin-top:8px" object-fit:cover;margin-top:6px" onerror="this.style.display='none'">
onerror="this.style.display='none'">
</div>` : ''} </div>` : ''}
${_resultBlock('📝 Caption', data.caption, true)} ${_resultBlock('📝 Caption', data.caption, true)}
${data.hashtags ? ` ${data.hashtags ? `
<div style="background:var(--c-surface);border:1px solid var(--c-border); <div class="card" style="padding:12px;margin-bottom:10px">
border-radius:var(--radius-lg);padding:var(--space-4);
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">🏷 Hashtags</div> <div class="sm-label">🏷 Hashtags</div>
<div style="font-size:var(--text-sm);color:var(--c-primary);margin-bottom:var(--space-3); <div style="font-size:var(--text-sm);color:var(--c-primary);margin-bottom:8px;
line-height:1.9;word-break:break-word"> line-height:1.8;word-break:break-word">
${data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' ')}</div> ${data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' ')}</div>
${_copyBtn(data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' '))} ${_copyBtn(data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' '))}
</div>` : ''} </div>` : ''}
${(data.hook||data.cta) ? ` ${(data.hook||data.cta) ? `
<div style="background:var(--c-surface);border:1px solid var(--c-border); <div class="card" style="padding:12px;margin-bottom:10px">
border-radius:var(--radius-lg);padding:var(--space-4);
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
${data.hook ? `<div class="sm-label">🎣 Hook</div> ${data.hook ? `<div class="sm-label">🎣 Hook</div>
<div style="font-size:var(--text-sm);font-style:italic;margin-bottom:var(--space-3); <div style="font-size:var(--text-sm);font-style:italic;margin-bottom:8px">
line-height:1.6">
"${_esc(data.hook)}"</div>` : ''} "${_esc(data.hook)}"</div>` : ''}
${data.cta ? `<div class="sm-label">📣 Call-to-Action</div> ${data.cta ? `<div class="sm-label">📣 Call-to-Action</div>
<div style="font-size:var(--text-sm);line-height:1.6">${_esc(data.cta)}</div>` : ''} <div style="font-size:var(--text-sm)">${_esc(data.cta)}</div>` : ''}
</div>` : ''} </div>` : ''}
${_resultBlock('📸 Was du filmen/fotografieren solltest', data.visual_brief, false)} ${_resultBlock('📸 Was du filmen/fotografieren solltest', data.visual_brief, false)}
${data.script ? ` ${data.script ? `
<div style="background:var(--c-surface);border:1px solid var(--c-border); <div class="card" style="padding:12px;margin-bottom:10px">
border-radius:var(--radius-lg);padding:var(--space-4);
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">🎬 Video-Aufbau</div> <div class="sm-label">🎬 Video-Aufbau</div>
<div style="font-size:var(--text-sm);white-space:pre-wrap; <div style="font-size:var(--text-sm);white-space:pre-wrap;
line-height:1.7">${_esc(data.script)}</div> line-height:1.5">${_esc(data.script)}</div>
</div>` : ''} </div>` : ''}
${(data.image_prompt||data.canva_notes||unsplash) ? ` ${(data.image_prompt||data.canva_notes||unsplash) ? `
<div style="background:var(--c-surface);border:1px solid var(--c-border); <div class="card" style="padding:12px;margin-bottom:10px">
border-radius:var(--radius-lg);padding:var(--space-4);
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">🛠 Wenn du kein eigenes Bild hast</div> <div class="sm-label">🛠 Wenn du kein eigenes Bild hast</div>
${data.image_prompt ? ` ${data.image_prompt ? `
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:6px"> <div style="font-size:11px;color:var(--c-text-muted);margin-bottom:4px">
DALL-E / Midjourney:</div> DALL-E / Midjourney:</div>
<div style="font-size:11px;background:var(--c-surface-2);padding:10px; <div style="font-size:11px;background:var(--c-surface-2);padding:8px;
border-radius:var(--radius-md);font-family:monospace;word-break:break-word; border-radius:6px;font-family:monospace;word-break:break-word;
margin-bottom:var(--space-3);line-height:1.5">${_esc(data.image_prompt)}</div> margin-bottom:8px">${_esc(data.image_prompt)}</div>
${_copyBtn(data.image_prompt)}` : ''} ${_copyBtn(data.image_prompt)}` : ''}
${data.canva_notes ? ` ${data.canva_notes ? `
<div style="font-size:11px;color:var(--c-text-muted);margin:var(--space-3) 0 6px">Canva:</div> <div style="font-size:11px;color:var(--c-text-muted);margin:8px 0 4px">Canva:</div>
<div style="font-size:var(--text-sm);margin-bottom:var(--space-3); <div style="font-size:var(--text-sm);margin-bottom:8px">${_esc(data.canva_notes)}</div>` : ''}
line-height:1.6">${_esc(data.canva_notes)}</div>` : ''}
${unsplash ? `<a href="${unsplash}" target="_blank" rel="noopener" ${unsplash ? `<a href="${unsplash}" target="_blank" rel="noopener"
style="font-size:var(--text-sm);color:var(--c-primary);display:inline-block"> style="font-size:var(--text-sm);color:var(--c-primary);display:inline-block">
🔍 Kostenlose Fotos auf Unsplash </a>` : ''} 🔍 Kostenlose Fotos auf Unsplash </a>` : ''}
@ -908,12 +833,10 @@ window.Page_social = (() => {
function _resultBlock(label, text, copyable) { function _resultBlock(label, text, copyable) {
if (!text) return ''; if (!text) return '';
return `<div style="background:var(--c-surface);border:1px solid var(--c-border); return `<div class="card" style="padding:12px;margin-bottom:10px">
border-radius:var(--radius-lg);padding:var(--space-4);
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">${label}</div> <div class="sm-label">${label}</div>
<div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.7; <div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.6;
margin-bottom:${copyable?'var(--space-3)':'0'}">${_esc(text)}</div> margin-bottom:${copyable?'8px':'0'}">${_esc(text)}</div>
${copyable ? _copyBtn(text) : ''} ${copyable ? _copyBtn(text) : ''}
</div>`; </div>`;
} }
@ -921,8 +844,7 @@ window.Page_social = (() => {
function _copyBtn(text) { function _copyBtn(text) {
return `<button class="btn btn-sm btn-secondary sm-copy" return `<button class="btn btn-sm btn-secondary sm-copy"
data-copy="${_esc(text)}" data-copy="${_esc(text)}"
style="font-size:11px;padding:5px 14px;min-height:32px; style="font-size:11px;padding:4px 10px;min-height:30px">
border-radius:var(--radius-full)">
📋 Kopieren</button>`; 📋 Kopieren</button>`;
} }
@ -1101,11 +1023,10 @@ window.Page_social = (() => {
Tippe auf 📤 wenn du einen Post abgesetzt hast so lernt Luna was wirklich live ging.</div> Tippe auf 📤 wenn du einen Post abgesetzt hast so lernt Luna was wirklich live ging.</div>
</div> </div>
</div>` : ''} </div>` : ''}
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-4)"> <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-3)">
${['alle','idea','draft','scheduled','published','archived'].map(s => ` ${['alle','idea','draft','scheduled','published','archived'].map(s => `
<button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}" <button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}"
data-f="${s}" style="padding:4px 12px;font-size:11px;min-height:30px; data-f="${s}" style="padding:3px 10px;font-size:11px;min-height:30px">
border-radius:var(--radius-full)">
${fLabel[s]}</button>`).join('')} ${fLabel[s]}</button>`).join('')}
</div> </div>
${!items.length ${!items.length
@ -1218,22 +1139,19 @@ window.Page_social = (() => {
function _renderBewerten(el) { function _renderBewerten(el) {
let selPlatform = 'instagram'; let selPlatform = 'instagram';
el.innerHTML = ` el.innerHTML = `
<div style="background:var(--c-surface);border-radius:var(--radius-lg); <div style="background:var(--c-surface-2);
box-shadow:var(--shadow-sm);padding:var(--space-4); border-radius:12px;padding:12px;margin-bottom:var(--space-3);
margin-bottom:var(--space-4); display:flex;gap:8px">
display:flex;gap:var(--space-3)"> <span style="font-size:1.2em;flex-shrink:0">🌙</span>
<span style="font-size:1.4em;flex-shrink:0">🌙</span> <div style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.6">
Zeig mir deinen Entwurf ich sage dir was gut ist und wie du ihn Zeig mir deinen Entwurf ich sage dir was gut ist und wie du ihn
noch besser machen kannst!</div> noch besser machen kannst!</div>
</div> </div>
<div style="background:var(--c-surface);border-radius:var(--radius-lg); <div class="card" style="padding:var(--space-3)">
box-shadow:var(--shadow-sm);padding:var(--space-4)"> <div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-3)">
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4)">
${['instagram','tiktok','both'].map((p,i) => ` ${['instagram','tiktok','both'].map((p,i) => `
<button class="btn btn-sm sm-ep ${i===0?'btn-primary':'btn-secondary'}" <button class="btn btn-sm sm-ep ${i===0?'btn-primary':'btn-secondary'}"
data-p="${p}" style="flex:1;min-height:36px;font-size:12px;padding:4px 8px; data-p="${p}" style="flex:1;min-height:38px;font-size:12px;padding:4px">
border-radius:var(--radius-full)">
${_PL[p]}</button>`).join('')} ${_PL[p]}</button>`).join('')}
</div> </div>
<textarea id="sm-draft" class="input" <textarea id="sm-draft" class="input"
@ -1241,12 +1159,11 @@ window.Page_social = (() => {
font-size:var(--text-sm);line-height:1.5" font-size:var(--text-sm);line-height:1.5"
placeholder="Schreib hier deinen Caption-Entwurf oder einfach worum es im Post geht…"></textarea> placeholder="Schreib hier deinen Caption-Entwurf oder einfach worum es im Post geht…"></textarea>
<button id="sm-eval" class="btn btn-primary" <button id="sm-eval" class="btn btn-primary"
style="margin-top:var(--space-4);width:100%;min-height:52px; style="margin-top:var(--space-3);width:100%;min-height:48px;
font-size:var(--text-base);border-radius:var(--radius-lg); font-size:var(--text-base)">
box-shadow:var(--shadow-md)">
🔍 Luna, schau mal drüber! 🔍 Luna, schau mal drüber!
</button> </button>
<div id="sm-eval-res" style="margin-top:var(--space-4)"></div> <div id="sm-eval-res" style="margin-top:var(--space-3)"></div>
</div>`; </div>`;
el.querySelectorAll('.sm-ep').forEach(b => b.addEventListener('click', () => { el.querySelectorAll('.sm-ep').forEach(b => b.addEventListener('click', () => {
@ -1271,16 +1188,15 @@ window.Page_social = (() => {
_progressDone(res); _progressDone(res);
await new Promise(r => setTimeout(r, 400)); await new Promise(r => setTimeout(r, 400));
res.innerHTML = ` res.innerHTML = `
${data.notes ? `<div style="background:var(--c-primary-subtle); ${data.notes ? `<div style="background:var(--c-surface-2);
border-radius:var(--radius-lg);padding:var(--space-4);margin-bottom:var(--space-3); border-radius:12px;padding:12px;margin-bottom:12px;
border-left:4px solid var(--c-primary);box-shadow:var(--shadow-xs)"> border-left:3px solid var(--c-primary)">
<div style="display:flex;gap:var(--space-3)"> <div style="display:flex;gap:8px">
<span style="font-size:1.3em;flex-shrink:0">🌙</span> <span>🌙</span>
<div> <div>
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px; <div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:3px">
text-transform:uppercase;letter-spacing:.5px">
Lunas Feedback:</div> Lunas Feedback:</div>
<div style="font-size:var(--text-sm);line-height:1.6">${_esc(data.notes)}</div> <div style="font-size:var(--text-sm);line-height:1.5">${_esc(data.notes)}</div>
</div> </div>
</div> </div>
</div>` : ''} </div>` : ''}
@ -1321,14 +1237,6 @@ window.Page_social = (() => {
.sm-label{font-size:11px;font-weight:700;color:var(--c-text-muted); .sm-label{font-size:11px;font-weight:700;color:var(--c-text-muted);
text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;display:block} text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;display:block}
@keyframes luna-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}} @keyframes luna-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
#sm-breed-day:hover,#sm-training-tip:hover,#sm-pflege-tip:hover{
transform:translateY(-2px);box-shadow:var(--shadow-md)!important}
#sm-breed-day:active,#sm-training-tip:active,#sm-pflege-tip:active{
transform:translateY(0)}
#sm-show-exercises:hover{background:var(--c-surface-2)!important;
border-color:var(--c-border)!important;color:var(--c-text-secondary)!important}
.sm-breed-chip:hover{background:var(--c-primary-subtle)!important;
border-color:var(--c-primary)!important;color:var(--c-primary-dark)!important}
`; `;
document.head.appendChild(style); document.head.appendChild(style);

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v359'; const CACHE_VERSION = 'by-v356';
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten