feat: zentrales Pushover-Logging + neuer Admin-Tab Logs
This commit is contained in:
@@ -636,4 +636,18 @@ db.exec(`
|
||||
)
|
||||
`);
|
||||
|
||||
// Log aller verschickten Pushover-Nachrichten (für Admin-Übersicht "Logs")
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS push_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
message TEXT NOT NULL DEFAULT '',
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
success INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
module.exports = db;
|
||||
|
||||
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
require('./db');
|
||||
const { logPush } = require('./pushLog');
|
||||
|
||||
const app = express();
|
||||
// Hinter Nginx Proxy Manager (SSL-Terminierung) — sonst meldet req.protocol
|
||||
@@ -32,22 +33,25 @@ app.use((req, res, next) => {
|
||||
if (wasInactive && user?.role !== 'admin') {
|
||||
const username = db.prepare('SELECT username FROM users WHERE id=?').get(p.id)?.username || 'Jemand';
|
||||
const admins = db.prepare(`
|
||||
SELECT p.user_key, p.app_token FROM pushover_settings p
|
||||
SELECT p.user_id, p.user_key, p.app_token FROM pushover_settings p
|
||||
JOIN users u ON u.id = p.user_id
|
||||
WHERE u.role = 'admin' AND p.user_key IS NOT NULL AND p.app_token IS NOT NULL
|
||||
`).all();
|
||||
for (const admin of admins) {
|
||||
const title = '👤 DickenDock';
|
||||
const message = `${username} ist gerade online gegangen.`;
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: admin.app_token,
|
||||
user: admin.user_key,
|
||||
title: '👤 DickenDock',
|
||||
message: `${username} ist gerade online gegangen.`,
|
||||
title,
|
||||
message,
|
||||
priority: -1,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
logPush({ userId: admin.user_id, title, message, priority: -1, source: 'presence' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,11 +284,14 @@ mqttClient.setMediaAckAllHandler(() => {
|
||||
mqttClient.publishMediaAnfragen();
|
||||
});
|
||||
|
||||
async function sendPushoverMsg(userKey, appToken, message, opts = {}) {
|
||||
async function sendPushoverMsg(userKey, appToken, message, opts = {}, userId = null) {
|
||||
const title = 'DickenDock Erinnerung';
|
||||
let priority = 0;
|
||||
try {
|
||||
const params = { token: appToken, user: userKey, title: 'DickenDock Erinnerung', message };
|
||||
const params = { token: appToken, user: userKey, title, message };
|
||||
if (opts.retry && opts.expire) {
|
||||
params.priority = 2;
|
||||
priority = 2;
|
||||
params.retry = opts.retry;
|
||||
params.expire = opts.expire;
|
||||
}
|
||||
@@ -293,7 +300,11 @@ async function sendPushoverMsg(userKey, appToken, message, opts = {}) {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(params),
|
||||
});
|
||||
} catch(e) { console.error('Pushover Fehler:', e.message); }
|
||||
logPush({ userId, title, message, priority, source: 'dashboard-erinnerung' });
|
||||
} catch(e) {
|
||||
console.error('Pushover Fehler:', e.message);
|
||||
logPush({ userId, title, message, priority, source: 'dashboard-erinnerung', success: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Start auf die nächste volle Minute synchronisieren
|
||||
@@ -319,7 +330,7 @@ async function runScheduler() {
|
||||
`).all(now);
|
||||
for (const row of due) {
|
||||
await sendPushoverMsg(row.user_key, row.app_token, row.message,
|
||||
{ retry: row.retry, expire: row.expire });
|
||||
{ retry: row.retry, expire: row.expire }, row.user_id);
|
||||
db.prepare('UPDATE push_schedules SET sent=1 WHERE id=?').run(row.id);
|
||||
console.log(`✓ Push gesendet an user ${row.user_id}: "${row.message}" (fällig: ${row.scheduled_at})`);
|
||||
}
|
||||
|
||||
17
backend/src/pushLog.js
Normal file
17
backend/src/pushLog.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const db = require('./db');
|
||||
|
||||
// Wird von jeder Stelle im Code aufgerufen, die eine Pushover-Nachricht
|
||||
// verschickt — unabhängig vom Versand selbst, damit ein Fehler im Logging
|
||||
// nie den eigentlichen Push verhindert.
|
||||
function logPush({ userId = null, title = '', message = '', priority = 0, source = '', success = true }) {
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO push_log (user_id, title, message, priority, source, success, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, datetime('now','localtime'))
|
||||
`).run(userId, title, message, priority ?? 0, source, success ? 1 : 0);
|
||||
} catch (e) {
|
||||
console.error('Push-Log Fehler:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { logPush };
|
||||
@@ -3,6 +3,7 @@ const fs = require('fs');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const multer = require('multer');
|
||||
const db = require('../db');
|
||||
const { logPush } = require('../pushLog');
|
||||
const { authenticate, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -191,16 +192,25 @@ router.post('/users/:id/test-push', authenticate, requireAdmin, async (req, res)
|
||||
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(user.id);
|
||||
if (!poCfg?.app_token || !poCfg?.user_key) return res.status(400).json({ error: 'Kein Pushover eingerichtet' });
|
||||
const { message = 'Wat is mit meinen Fische?' } = req.body;
|
||||
const title = '📣 DockStation';
|
||||
try {
|
||||
const r = await fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: poCfg.app_token, user: poCfg.user_key, title: '📣 DockStation', message, priority: 0 }),
|
||||
body: JSON.stringify({ token: poCfg.app_token, user: poCfg.user_key, title, message, priority: 0 }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.status === 1) res.json({ ok: true });
|
||||
else res.status(500).json({ error: d.errors?.join(', ') || 'Pushover-Fehler' });
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
if (d.status === 1) {
|
||||
logPush({ userId: user.id, title, message, priority: 0, source: 'admin-test' });
|
||||
res.json({ ok: true });
|
||||
} else {
|
||||
logPush({ userId: user.id, title, message, priority: 0, source: 'admin-test', success: false });
|
||||
res.status(500).json({ error: d.errors?.join(', ') || 'Pushover-Fehler' });
|
||||
}
|
||||
} catch(e) {
|
||||
logPush({ userId: user.id, title, message, priority: 0, source: 'admin-test', success: false });
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── WebDAV / Synology NAS Einstellungen ───────────────────────────────────
|
||||
@@ -500,4 +510,18 @@ router.get('/disk-analysis', authenticate, requireAdmin, (req, res) => {
|
||||
res.json({ locations: result, tmpFiles, nodeModulesMb: fmt(nodeSize), bigFiles: bigFiles.slice(0,20) });
|
||||
});
|
||||
|
||||
// GET /push-logs – Übersicht aller verschickten Pushover-Nachrichten (Admin)
|
||||
router.get('/push-logs', authenticate, requireAdmin, (req, res) => {
|
||||
const limit = Math.min(parseInt(req.query.limit) || 200, 1000);
|
||||
const logs = db.prepare(`
|
||||
SELECT pl.id, pl.user_id, u.username, pl.title, pl.message, pl.priority,
|
||||
pl.source, pl.success, pl.created_at
|
||||
FROM push_log pl
|
||||
LEFT JOIN users u ON u.id = pl.user_id
|
||||
ORDER BY pl.id DESC
|
||||
LIMIT ?
|
||||
`).all(limit);
|
||||
res.json(logs);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { logPush } = require('../../pushLog');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
@@ -207,6 +208,7 @@ function sendPush(userId, title, message) {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ token:cfg.app_token, user:cfg.user_key, title, message, priority:0 }),
|
||||
}).catch(() => {});
|
||||
logPush({ userId, title, message, priority: 0, source: 'gebietseroberung' });
|
||||
}
|
||||
|
||||
// Spiel laden + Fog anwenden + echte Scores beifügen
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const https = require('https');
|
||||
const db = require('../../db');
|
||||
const mqttClient = require('../../mqtt');
|
||||
const { logPush } = require('../../pushLog');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
@@ -430,14 +431,15 @@ function formatOfferMessage(offers) {
|
||||
async function sendKoepiPushover(offers, onlyUserId = null) {
|
||||
const recipients = onlyUserId
|
||||
? db.prepare(`
|
||||
SELECT user_key, app_token FROM pushover_settings
|
||||
SELECT user_id, user_key, app_token FROM pushover_settings
|
||||
WHERE user_id = ? AND user_key IS NOT NULL AND app_token IS NOT NULL
|
||||
`).all(onlyUserId)
|
||||
: db.prepare(`
|
||||
SELECT user_key, app_token FROM pushover_settings
|
||||
SELECT user_id, user_key, app_token FROM pushover_settings
|
||||
WHERE user_key IS NOT NULL AND app_token IS NOT NULL
|
||||
`).all();
|
||||
const message = formatOfferMessage(offers);
|
||||
const title = '🍺 KöPi Angebote';
|
||||
for (const r of recipients) {
|
||||
try {
|
||||
await fetch('https://api.pushover.net/1/messages.json', {
|
||||
@@ -446,12 +448,16 @@ async function sendKoepiPushover(offers, onlyUserId = null) {
|
||||
body: new URLSearchParams({
|
||||
token: r.app_token,
|
||||
user: r.user_key,
|
||||
title: '🍺 KöPi Angebote',
|
||||
title,
|
||||
message,
|
||||
priority: 0,
|
||||
}),
|
||||
});
|
||||
} catch (e) { console.error('KöPi Pushover Fehler:', e.message); }
|
||||
logPush({ userId: r.user_id, title, message, priority: 0, source: 'koepi' });
|
||||
} catch (e) {
|
||||
console.error('KöPi Pushover Fehler:', e.message);
|
||||
logPush({ userId: r.user_id, title, message, priority: 0, source: 'koepi', success: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../../db');
|
||||
const { logPush } = require('../../pushLog');
|
||||
const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||
|
||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||
@@ -898,17 +899,20 @@ function ackFavorite(id) {
|
||||
try {
|
||||
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(fav.user_id);
|
||||
if (poCfg?.app_token && poCfg?.user_key) {
|
||||
const title = '✅ Favorit bestätigt';
|
||||
const message = `"${fav.title}" wurde von einem Admin bestätigt.`;
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: poCfg.app_token,
|
||||
user: poCfg.user_key,
|
||||
title: '✅ Favorit bestätigt',
|
||||
message: `"${fav.title}" wurde von einem Admin bestätigt.`,
|
||||
title,
|
||||
message,
|
||||
priority: 0,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
logPush({ userId: fav.user_id, title, message, priority: 0, source: 'media-favorit' });
|
||||
}
|
||||
} catch {}
|
||||
return fav;
|
||||
@@ -923,6 +927,7 @@ function ackAllFavorites() {
|
||||
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(userId);
|
||||
if (!poCfg?.app_token || !poCfg?.user_key) continue;
|
||||
const userFavs = toAck.filter(f => f.user_id === userId);
|
||||
const title = '✅ Favoriten bestätigt';
|
||||
const message = userFavs.length === 1
|
||||
? `"${userFavs[0].title}" wurde von einem Admin bestätigt.`
|
||||
: `${userFavs.length} deiner Favoriten wurden von einem Admin bestätigt.`;
|
||||
@@ -932,11 +937,12 @@ function ackAllFavorites() {
|
||||
body: JSON.stringify({
|
||||
token: poCfg.app_token,
|
||||
user: poCfg.user_key,
|
||||
title: '✅ Favoriten bestätigt',
|
||||
title,
|
||||
message,
|
||||
priority: 0,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
logPush({ userId, title, message, priority: 0, source: 'media-favorit' });
|
||||
}
|
||||
} catch {}
|
||||
return toAck.length;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { logPush } = require('../../pushLog');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// ── Pushover Helper ───────────────────────────────────────────────────────────
|
||||
async function sendPushover(userKey, appToken, title, message, opts = {}) {
|
||||
async function sendPushover(userKey, appToken, title, message, opts = {}, userId = null) {
|
||||
let priority = 0;
|
||||
try {
|
||||
const params = { token: appToken, user: userKey, title, message };
|
||||
if (opts.retry && opts.expire) {
|
||||
params.priority = 2;
|
||||
priority = 2;
|
||||
params.retry = opts.retry;
|
||||
params.expire = opts.expire;
|
||||
}
|
||||
@@ -17,7 +20,10 @@ async function sendPushover(userKey, appToken, title, message, opts = {}) {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(params),
|
||||
});
|
||||
} catch {}
|
||||
logPush({ userId, title, message, priority, source: 'nachrichten' });
|
||||
} catch {
|
||||
logPush({ userId, title, message, priority, source: 'nachrichten', success: false });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public Keys ───────────────────────────────────────────────────────────────
|
||||
@@ -137,7 +143,7 @@ router.post('/messages/:userId', authenticate, async (req, res) => {
|
||||
`).get(recipId);
|
||||
if (!presence) {
|
||||
await sendPushover(pushover.user_key, pushover.app_token, 'DickenDock', `Neue Nachricht von ${senderName}`,
|
||||
{ retry: pushover.retry, expire: pushover.expire });
|
||||
{ retry: pushover.retry, expire: pushover.expire }, recipId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +209,7 @@ router.post('/pushover/test', authenticate, async (req, res) => {
|
||||
? `🚨 Emergency-Test! Wiederholt alle ${row.retry}s für max. ${row.expire}s. Bitte in Pushover quittieren.`
|
||||
: 'Pushover-Verbindung erfolgreich! 🎉';
|
||||
await sendPushover(row.user_key, row.app_token, 'DickenDock', msg,
|
||||
useEmergency ? { retry: row.retry, expire: row.expire } : {});
|
||||
useEmergency ? { retry: row.retry, expire: row.expire } : {}, req.user.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { logPush } = require('../../pushLog');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
@@ -103,6 +104,7 @@ function sendPush(userId, title, message) {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ token:cfg.app_token, user:cfg.user_key, title, message, priority:0 }),
|
||||
}).catch(() => {});
|
||||
logPush({ userId, title, message, priority: 0, source: 'schocken' });
|
||||
}
|
||||
|
||||
const uid = req => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { logPush } = require('../../pushLog');
|
||||
const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
@@ -174,17 +175,19 @@ router.post('/:id/save', authenticate, async (req, res) => {
|
||||
for (const userId of notify) {
|
||||
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(userId);
|
||||
if (!poCfg?.app_token || !poCfg?.user_key) continue;
|
||||
const title = '🖊 Whiteboard aktualisiert';
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: poCfg.app_token,
|
||||
user: poCfg.user_key,
|
||||
title: '🖊 Whiteboard aktualisiert',
|
||||
title,
|
||||
message,
|
||||
priority: 0,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
logPush({ userId, title, message, priority: 0, source: 'whiteboard-save' });
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
@@ -227,17 +230,20 @@ router.put('/:id/permissions', authenticate, async (req, res) => {
|
||||
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(p.user_id);
|
||||
if (!poCfg?.app_token || !poCfg?.user_key) continue;
|
||||
const roleLabel = p.role === 'edit' ? 'bearbeiten' : 'ansehen';
|
||||
const title = '🖊 Whiteboard geteilt';
|
||||
const message = `${owner.username} hat das Whiteboard "${wb.title}" mit dir geteilt (${roleLabel}).`;
|
||||
fetch('https://api.pushover.net/1/messages.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: poCfg.app_token,
|
||||
user: poCfg.user_key,
|
||||
title: '🖊 Whiteboard geteilt',
|
||||
message: `${owner.username} hat das Whiteboard "${wb.title}" mit dir geteilt (${roleLabel}).`,
|
||||
title,
|
||||
message,
|
||||
priority: 0,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
logPush({ userId: p.user_id, title, message, priority: 0, source: 'whiteboard-share' });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
@@ -3185,6 +3185,95 @@ function WebDavSettings({ toast }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Admin: Log aller verschickten Pushover-Nachrichten
|
||||
const PRIORITY_LABELS = { '-2':'Silent', '-1':'Leise', '0':'Normal', '1':'Hoch', '2':'Emergency' };
|
||||
const PRIORITY_COLORS = { '-2':'#888888', '-1':'#4ecdc4', '0':'rgba(255,255,255,0.6)', '1':'#ffe66d', '2':'#ff6b9d' };
|
||||
|
||||
function PushLogs({ toast }) {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
api('/admin/push-logs?limit=300').then(setLogs).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const fmtDate = iso => {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso.replace(' ', 'T'));
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
||||
};
|
||||
|
||||
const filtered = filter.trim()
|
||||
? logs.filter(l => (l.username||'').toLowerCase().includes(filter.toLowerCase())
|
||||
|| (l.title||'').toLowerCase().includes(filter.toLowerCase())
|
||||
|| (l.message||'').toLowerCase().includes(filter.toLowerCase())
|
||||
|| (l.source||'').toLowerCase().includes(filter.toLowerCase()))
|
||||
: logs;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Sec title={`PUSHOVER-VERLAUF (${filtered.length})`}>
|
||||
<div style={{ display:'flex', gap:8, marginBottom:12 }}>
|
||||
<input value={filter} onChange={e=>setFilter(e.target.value)}
|
||||
placeholder="Filtern nach Benutzer, Text, Quelle…"
|
||||
style={{ ...S.inp, flex:1 }} />
|
||||
<button onClick={load} style={{ ...S.btn('#4ecdc4'), flexShrink:0 }}>↻</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:12 }}>Lädt…</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:12 }}>Keine Einträge.</div>
|
||||
) : (
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:6, maxHeight:'65vh', overflowY:'auto' }}>
|
||||
{filtered.map(l => (
|
||||
<div key={l.id} style={{
|
||||
border:'1px solid rgba(255,255,255,0.08)', borderRadius:8, padding:'8px 10px',
|
||||
background: l.success ? 'transparent' : 'rgba(248,113,113,0.06)',
|
||||
}}>
|
||||
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:3, flexWrap:'wrap' }}>
|
||||
<span style={{ color:'#4ecdc4', fontFamily:"'Space Mono',monospace", fontSize:11, fontWeight:700 }}>
|
||||
{l.username || `User #${l.user_id ?? '?'}`}
|
||||
</span>
|
||||
<span style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}>
|
||||
{fmtDate(l.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color:'rgba(255,255,255,0.85)', fontFamily:'monospace', fontSize:12, marginBottom:2 }}>
|
||||
{l.title}
|
||||
</div>
|
||||
<div style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:11, marginBottom:6, whiteSpace:'pre-wrap' }}>
|
||||
{l.message}
|
||||
</div>
|
||||
<div style={{ display:'flex', gap:8, flexWrap:'wrap', alignItems:'center' }}>
|
||||
<span style={{
|
||||
color: PRIORITY_COLORS[String(l.priority)] || 'rgba(255,255,255,0.5)',
|
||||
fontFamily:'monospace', fontSize:9, letterSpacing:1,
|
||||
border:`1px solid ${PRIORITY_COLORS[String(l.priority)] || 'rgba(255,255,255,0.2)'}`,
|
||||
borderRadius:4, padding:'1px 6px',
|
||||
}}>
|
||||
{PRIORITY_LABELS[String(l.priority)] || l.priority}
|
||||
</span>
|
||||
<span style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9 }}>
|
||||
{l.source || '—'}
|
||||
</span>
|
||||
{!l.success && (
|
||||
<span style={{ color:'#f87171', fontFamily:'monospace', fontSize:9 }}>✕ fehlgeschlagen</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Sec>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminPanel({ toast, mobile, user, nav }) {
|
||||
const [section, setSection] = useState('profil');
|
||||
useEffect(() => { if (nav?.section) setSection(nav.section); }, [nav?.ts]);
|
||||
@@ -3246,7 +3335,7 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
||||
</div>
|
||||
|
||||
<div style={{ display:'flex', gap:6, marginBottom:18, flexWrap:'wrap' }}>
|
||||
{[['profil','Profil'],['sicherheit','Sicherheit'],['dashboard','Dashboard'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup']]:[])]
|
||||
{[['profil','Profil'],['sicherheit','Sicherheit'],['dashboard','Dashboard'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup'],['logs','Logs']]:[])]
|
||||
.map(([k,l]) => (
|
||||
<button key={k} onClick={()=>setSection(k)} style={{
|
||||
padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
|
||||
@@ -3382,6 +3471,10 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
||||
</Sec>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section === 'logs' && user?.role==='admin' && (
|
||||
<PushLogs toast={toast}/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user