Feature: Moderation History — Log für alle 4 Bereiche, resolved_by/at Migration, SW by-v590
This commit is contained in:
parent
e2cd32a550
commit
87039994ce
6 changed files with 125 additions and 43 deletions
|
|
@ -1072,6 +1072,19 @@ def _migrate(conn_factory):
|
||||||
pass
|
pass
|
||||||
logger.info("Migration: wiki_rassen Anreicherungs-Felder bereit.")
|
logger.info("Migration: wiki_rassen Anreicherungs-Felder bereit.")
|
||||||
|
|
||||||
|
# Moderation-Logging: resolved_by/at für forum_reports, verified_by/at/reject für wiki_zuchter
|
||||||
|
for table, col, typedef in [
|
||||||
|
("forum_reports", "resolved_by", "INTEGER"),
|
||||||
|
("forum_reports", "resolved_at", "TEXT"),
|
||||||
|
("wiki_zuchter", "verified_by", "INTEGER"),
|
||||||
|
("wiki_zuchter", "verified_at", "TEXT"),
|
||||||
|
("wiki_zuchter", "reject_reason", "TEXT"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Wiki: Züchter-Verzeichnis
|
# Wiki: Züchter-Verzeichnis
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS wiki_zuchter (
|
CREATE TABLE IF NOT EXISTS wiki_zuchter (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""BAN YARO — Moderations-Panel Backend"""
|
"""BAN YARO — Moderations-Panel Backend"""
|
||||||
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from database import db
|
from database import db
|
||||||
from auth import get_current_user
|
from auth import get_current_user
|
||||||
|
|
@ -69,17 +70,19 @@ async def mod_stats(user=Depends(require_moderator)):
|
||||||
async def mod_reports(user=Depends(require_moderator)):
|
async def mod_reports(user=Depends(require_moderator)):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute("""
|
||||||
SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at,
|
SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved,
|
||||||
u.name AS melder_name,
|
r.created_at, r.resolved_at,
|
||||||
|
u.name AS melder_name,
|
||||||
|
m.name AS resolved_by_name,
|
||||||
CASE r.target_type
|
CASE r.target_type
|
||||||
WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id)
|
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)
|
WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id)
|
||||||
END AS content_preview
|
END AS content_preview
|
||||||
FROM forum_reports r
|
FROM forum_reports r
|
||||||
LEFT JOIN users u ON u.id=r.user_id
|
LEFT JOIN users u ON u.id=r.user_id
|
||||||
WHERE r.resolved=0
|
LEFT JOIN users m ON m.id=r.resolved_by
|
||||||
ORDER BY r.created_at DESC
|
ORDER BY r.resolved ASC, r.created_at DESC
|
||||||
LIMIT 100
|
LIMIT 200
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
@ -97,8 +100,12 @@ async def mod_resolve_report(rid: int, user=Depends(require_moderator)):
|
||||||
raise HTTPException(404, "Meldung nicht gefunden.")
|
raise HTTPException(404, "Meldung nicht gefunden.")
|
||||||
new_state = 0 if r["resolved"] else 1
|
new_state = 0 if r["resolved"] else 1
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE forum_reports SET resolved=? WHERE id=?",
|
"""UPDATE forum_reports SET resolved=?, resolved_by=?, resolved_at=?
|
||||||
(new_state, rid)
|
WHERE id=?""",
|
||||||
|
(new_state,
|
||||||
|
user["id"] if new_state else None,
|
||||||
|
datetime.utcnow().isoformat() if new_state else None,
|
||||||
|
rid)
|
||||||
)
|
)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
@ -189,17 +196,19 @@ async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)):
|
||||||
async def mod_fotos(user=Depends(require_moderator)):
|
async def mod_fotos(user=Depends(require_moderator)):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute("""
|
||||||
SELECT s.id, s.foto_url, s.created_at,
|
SELECT s.id, s.foto_url, s.status, s.created_at,
|
||||||
|
s.reviewed_at, s.reject_reason,
|
||||||
COALESCE(s.rights_confirmed, 0) AS rights_confirmed,
|
COALESCE(s.rights_confirmed, 0) AS rights_confirmed,
|
||||||
u.name AS user_name,
|
u.name AS user_name,
|
||||||
r.name AS rasse_name, r.slug AS rasse_slug,
|
m.name AS reviewed_by_name,
|
||||||
|
r.name AS rasse_name, r.slug AS rasse_slug,
|
||||||
r.foto_url AS aktuell_foto
|
r.foto_url AS aktuell_foto
|
||||||
FROM wiki_foto_submissions s
|
FROM wiki_foto_submissions s
|
||||||
LEFT JOIN users u ON u.id = s.user_id
|
LEFT JOIN users u ON u.id = s.user_id
|
||||||
|
LEFT JOIN users m ON m.id = s.reviewed_by
|
||||||
LEFT JOIN wiki_rassen r ON r.id = s.rasse_id
|
LEFT JOIN wiki_rassen r ON r.id = s.rasse_id
|
||||||
WHERE s.status = 'pending'
|
ORDER BY s.status ASC, s.created_at ASC
|
||||||
ORDER BY s.created_at ASC
|
LIMIT 200
|
||||||
LIMIT 50
|
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
@ -228,11 +237,13 @@ async def mod_poi_edits(user=Depends(require_moderator)):
|
||||||
SELECT e.id, e.osm_id, e.poi_name, e.field,
|
SELECT e.id, e.osm_id, e.poi_name, e.field,
|
||||||
e.old_value, e.new_value, e.status,
|
e.old_value, e.new_value, e.status,
|
||||||
e.created_at, e.resolved_at,
|
e.created_at, e.resolved_at,
|
||||||
u.name AS einreicher_name
|
u.name AS einreicher_name,
|
||||||
|
m.name AS mod_name
|
||||||
FROM osm_poi_edits e
|
FROM osm_poi_edits e
|
||||||
JOIN users u ON u.id = e.user_id
|
JOIN users u ON u.id = e.user_id
|
||||||
|
LEFT JOIN users m ON m.id = e.mod_id
|
||||||
ORDER BY e.status ASC, e.created_at DESC
|
ORDER BY e.status ASC, e.created_at DESC
|
||||||
LIMIT 100
|
LIMIT 200
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -694,11 +694,12 @@ async def list_zuchter_pending(user=Depends(get_current_user)):
|
||||||
raise HTTPException(403, "Nur Moderatoren.")
|
raise HTTPException(403, "Nur Moderatoren.")
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""SELECT z.*, u.name AS user_name
|
"""SELECT z.*, u.name AS user_name, m.name AS verified_by_name
|
||||||
FROM wiki_zuchter z
|
FROM wiki_zuchter z
|
||||||
LEFT JOIN users u ON u.id = z.user_id
|
LEFT JOIN users u ON u.id = z.user_id
|
||||||
WHERE z.verified=0
|
LEFT JOIN users m ON m.id = z.verified_by
|
||||||
ORDER BY z.created_at ASC""",
|
ORDER BY z.verified ASC, z.created_at ASC
|
||||||
|
LIMIT 200""",
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
@ -716,8 +717,10 @@ async def verify_zuchter(zuchter_id: int, user=Depends(get_current_user)):
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(404, "Züchter nicht gefunden.")
|
raise HTTPException(404, "Züchter nicht gefunden.")
|
||||||
|
from datetime import datetime
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE wiki_zuchter SET verified=1 WHERE id=?", (zuchter_id,)
|
"UPDATE wiki_zuchter SET verified=1, verified_by=?, verified_at=? WHERE id=?",
|
||||||
|
(user["id"], datetime.utcnow().isoformat(), zuchter_id)
|
||||||
)
|
)
|
||||||
result = conn.execute(
|
result = conn.execute(
|
||||||
"SELECT * FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
"SELECT * FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '589'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '590'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1460,6 +1460,30 @@ window.Page_admin = (() => {
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// TAB: MODERATION
|
// TAB: MODERATION
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
function _historySection(label, items, renderItem) {
|
||||||
|
const id = `hist-${label.replace(/\W/g,'').toLowerCase()}`;
|
||||||
|
return `
|
||||||
|
<details style="margin-bottom:var(--space-4)">
|
||||||
|
<summary style="cursor:pointer;list-style:none;display:flex;align-items:center;gap:var(--space-2);
|
||||||
|
font-size:var(--text-xs);font-weight:700;color:var(--c-text-muted);
|
||||||
|
text-transform:uppercase;letter-spacing:.06em;padding:var(--space-2) 0;
|
||||||
|
border-top:1px solid var(--c-border)">
|
||||||
|
${UI.icon('clock-countdown')} ${items.length} erledigte ${label}
|
||||||
|
<svg class="ph-icon" style="margin-left:auto;transition:transform .2s" aria-hidden="true">
|
||||||
|
<use href="/icons/phosphor.svg#caret-down"></use>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top:var(--space-2);display:flex;flex-direction:column;gap:var(--space-1)">
|
||||||
|
${items.map(item => `
|
||||||
|
<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);display:flex;align-items:center;flex-wrap:wrap;gap:4px">
|
||||||
|
${renderItem(item)}
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</details>`;
|
||||||
|
}
|
||||||
|
|
||||||
async function _renderModeration(el) {
|
async function _renderModeration(el) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
|
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
|
||||||
|
|
@ -1480,13 +1504,20 @@ window.Page_admin = (() => {
|
||||||
API.get('/moderation/reports').catch(() => []),
|
API.get('/moderation/reports').catch(() => []),
|
||||||
API.get('/moderation/poi-edits').catch(() => []),
|
API.get('/moderation/poi-edits').catch(() => []),
|
||||||
]);
|
]);
|
||||||
const poiPending = poiEdits.filter(e => e.status === 'pending');
|
const zuchterPending = zuchter.filter(z => !z.verified);
|
||||||
|
const zuchterDone = zuchter.filter(z => z.verified);
|
||||||
|
const fotosPending = fotos.filter(f => f.status === 'pending');
|
||||||
|
const fotosDone = fotos.filter(f => f.status !== 'pending');
|
||||||
|
const reportsPending = reports.filter(r => !r.resolved);
|
||||||
|
const reportsDone = reports.filter(r => r.resolved);
|
||||||
|
const poiPending = poiEdits.filter(e => e.status === 'pending');
|
||||||
|
const poiDone = poiEdits.filter(e => e.status !== 'pending');
|
||||||
|
|
||||||
const modItems = [
|
const modItems = [
|
||||||
{ label: 'Züchter-Einreichungen', count: zuchter.length, icon: 'certificate' },
|
{ label: 'Züchter-Einreichungen', count: zuchterPending.length, icon: 'certificate' },
|
||||||
{ label: 'Foto-Einreichungen', count: fotos.length, icon: 'image' },
|
{ label: 'Foto-Einreichungen', count: fotosPending.length, icon: 'image' },
|
||||||
{ label: 'Forum-Meldungen', count: reports.length, icon: 'warning' },
|
{ label: 'Forum-Meldungen', count: reportsPending.length, icon: 'warning' },
|
||||||
{ label: 'POI-Korrekturen', count: poiPending.length, icon: 'map-pin' },
|
{ label: 'POI-Korrekturen', count: poiPending.length, icon: 'map-pin' },
|
||||||
].filter(i => i.count > 0);
|
].filter(i => i.count > 0);
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
|
|
@ -1520,18 +1551,18 @@ window.Page_admin = (() => {
|
||||||
margin-bottom:var(--space-3)">
|
margin-bottom:var(--space-3)">
|
||||||
Züchter-Einreichungen
|
Züchter-Einreichungen
|
||||||
<span style="background:var(--c-primary);color:#fff;border-radius:999px;
|
<span style="background:var(--c-primary);color:#fff;border-radius:999px;
|
||||||
padding:1px 8px;font-size:var(--text-xs);margin-left:6px">${zuchter.length}</span>
|
padding:1px 8px;font-size:var(--text-xs);margin-left:6px">${zuchterPending.length}</span>
|
||||||
</h3>`;
|
</h3>`;
|
||||||
|
|
||||||
if (!zuchter.length) {
|
if (!zuchterPending.length) {
|
||||||
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-bottom:var(--space-6)">Keine ausstehenden Einreichungen.</p>`;
|
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-bottom:var(--space-3)">Keine ausstehenden Einreichungen.</p>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div class="card adm-table-card" style="margin-bottom:var(--space-6)"><div class="adm-table-scroll"><table class="adm-table">
|
html += `<div class="card adm-table-card" style="margin-bottom:var(--space-3)"><div class="adm-table-scroll"><table class="adm-table">
|
||||||
<thead><tr style="background:var(--c-surface-2);text-align:left">
|
<thead><tr style="background:var(--c-surface-2);text-align:left">
|
||||||
<th class="adm-th">Rasse</th><th class="adm-th">Name / Zwingername</th>
|
<th class="adm-th">Rasse</th><th class="adm-th">Name / Zwingername</th>
|
||||||
<th class="adm-th">Ort</th><th class="adm-th">VDH</th><th class="adm-th">Website</th><th class="adm-th"></th>
|
<th class="adm-th">Ort</th><th class="adm-th">VDH</th><th class="adm-th">Website</th><th class="adm-th"></th>
|
||||||
</tr></thead><tbody>
|
</tr></thead><tbody>
|
||||||
${zuchter.map((z, i) => `
|
${zuchterPending.map((z, i) => `
|
||||||
<tr style="${i%2===1?'background:var(--c-surface-2)':''}">
|
<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" 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.name)}${z.zwingername ? `<br><span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(z.zwingername)}</span>` : ''}</td>
|
||||||
|
|
@ -1545,6 +1576,10 @@ window.Page_admin = (() => {
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
</tbody></table></div></div>`;
|
</tbody></table></div></div>`;
|
||||||
}
|
}
|
||||||
|
// 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)}`);
|
||||||
|
|
||||||
// --- Wiki-Foto-Einreichungen ---
|
// --- Wiki-Foto-Einreichungen ---
|
||||||
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
|
|
@ -1552,14 +1587,14 @@ window.Page_admin = (() => {
|
||||||
margin-bottom:var(--space-3)">
|
margin-bottom:var(--space-3)">
|
||||||
Wiki-Foto-Einreichungen
|
Wiki-Foto-Einreichungen
|
||||||
<span style="background:var(--c-primary);color:#fff;border-radius:999px;
|
<span style="background:var(--c-primary);color:#fff;border-radius:999px;
|
||||||
padding:1px 8px;font-size:var(--text-xs);margin-left:6px">${fotos.length}</span>
|
padding:1px 8px;font-size:var(--text-xs);margin-left:6px">${fotosPending.length}</span>
|
||||||
</h3>`;
|
</h3>`;
|
||||||
|
|
||||||
if (!fotos.length) {
|
if (!fotosPending.length) {
|
||||||
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted)">Keine ausstehenden Foto-Einreichungen.</p>`;
|
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-bottom:var(--space-3)">Keine ausstehenden Foto-Einreichungen.</p>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-4)">
|
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-4);margin-bottom:var(--space-3)">
|
||||||
${fotos.map(f => `
|
${fotosPending.map(f => `
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)">
|
||||||
<img src="${_esc(f.foto_url)}" alt=""
|
<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)">
|
style="width:100%;height:140px;object-fit:cover;border-radius:var(--radius-md);margin-bottom:var(--space-3)">
|
||||||
|
|
@ -1577,21 +1612,28 @@ window.Page_admin = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.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)}`);
|
||||||
|
|
||||||
// --- Forum-Meldungen ---
|
// --- Forum-Meldungen ---
|
||||||
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em;
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em;
|
||||||
margin:var(--space-6) 0 var(--space-3)">
|
margin:var(--space-4) 0 var(--space-3)">
|
||||||
Forum-Meldungen
|
Forum-Meldungen
|
||||||
<span style="background:${reports.length ? 'var(--c-danger)' : 'var(--c-primary)'};color:#fff;
|
<span style="background:${reportsPending.length ? 'var(--c-danger)' : 'var(--c-primary)'};color:#fff;
|
||||||
border-radius:999px;padding:1px 8px;font-size:var(--text-xs);margin-left:6px">
|
border-radius:999px;padding:1px 8px;font-size:var(--text-xs);margin-left:6px">
|
||||||
${reports.length}
|
${reportsPending.length}
|
||||||
</span>
|
</span>
|
||||||
</h3>`;
|
</h3>`;
|
||||||
if (!reports.length) {
|
if (!reportsPending.length) {
|
||||||
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-bottom:var(--space-6)">Keine offenen Meldungen.</p>`;
|
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-bottom:var(--space-3)">Keine offenen Meldungen.</p>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div style="display:flex;flex-direction:column;gap:var(--space-3);margin-bottom:var(--space-6)">
|
html += `<div style="display:flex;flex-direction:column;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||||
${reports.map(r => `
|
${reportsPending.map(r => `
|
||||||
<div class="card" style="padding:var(--space-4);border-left:3px solid var(--c-danger)">
|
<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="display:flex;align-items:flex-start;gap:var(--space-3)">
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0">
|
||||||
|
|
@ -1614,6 +1656,11 @@ window.Page_admin = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)}`);
|
||||||
|
|
||||||
// --- POI-Korrekturen ---
|
// --- POI-Korrekturen ---
|
||||||
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em;
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em;
|
||||||
|
|
@ -1658,6 +1705,14 @@ window.Page_admin = (() => {
|
||||||
</table>
|
</table>
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
}
|
}
|
||||||
|
// 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 style="font-size:var(--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.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)}`);
|
||||||
|
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v589';
|
const CACHE_VERSION = 'by-v590';
|
||||||
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
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue