diff --git a/backend/src/db.js b/backend/src/db.js index bb3a39e..bfa933d 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -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; diff --git a/backend/src/index.js b/backend/src/index.js index 2a9e9d5..9721fee 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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})`); } diff --git a/backend/src/pushLog.js b/backend/src/pushLog.js new file mode 100644 index 0000000..8d25dfb --- /dev/null +++ b/backend/src/pushLog.js @@ -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 }; diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 3030dec..d2b8167 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -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; diff --git a/backend/src/tools/gebietseroberung/routes.js b/backend/src/tools/gebietseroberung/routes.js index 5640438..97d81e9 100644 --- a/backend/src/tools/gebietseroberung/routes.js +++ b/backend/src/tools/gebietseroberung/routes.js @@ -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 diff --git a/backend/src/tools/koepi/routes.js b/backend/src/tools/koepi/routes.js index 43c0b48..9ad9e3b 100644 --- a/backend/src/tools/koepi/routes.js +++ b/backend/src/tools/koepi/routes.js @@ -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 }); + } } } diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 89530ae..180880b 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -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; diff --git a/backend/src/tools/nachrichten/routes.js b/backend/src/tools/nachrichten/routes.js index 3ee9f1f..c170973 100644 --- a/backend/src/tools/nachrichten/routes.js +++ b/backend/src/tools/nachrichten/routes.js @@ -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 }); }); diff --git a/backend/src/tools/schocken/routes.js b/backend/src/tools/schocken/routes.js index 8823991..a758566 100644 --- a/backend/src/tools/schocken/routes.js +++ b/backend/src/tools/schocken/routes.js @@ -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 => { diff --git a/backend/src/tools/whiteboard/routes.js b/backend/src/tools/whiteboard/routes.js index b7c3d06..b4fff99 100644 --- a/backend/src/tools/whiteboard/routes.js +++ b/backend/src/tools/whiteboard/routes.js @@ -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 {} } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a25fb97..36003c9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 ( +
+ +
+ setFilter(e.target.value)} + placeholder="Filtern nach Benutzer, Text, Quelle…" + style={{ ...S.inp, flex:1 }} /> + +
+ + {loading ? ( +
LΓ€dt…
+ ) : filtered.length === 0 ? ( +
Keine EintrΓ€ge.
+ ) : ( +
+ {filtered.map(l => ( +
+
+ + {l.username || `User #${l.user_id ?? '?'}`} + + + {fmtDate(l.created_at)} + +
+
+ {l.title} +
+
+ {l.message} +
+
+ + {PRIORITY_LABELS[String(l.priority)] || l.priority} + + + {l.source || 'β€”'} + + {!l.success && ( + βœ• fehlgeschlagen + )} +
+
+ ))} +
+ )} +
+
+ ); +} + 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 }) {
- {[['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]) => (
)} + + {section === 'logs' && user?.role==='admin' && ( + + )} ); }