feat: zentrales Pushover-Logging + neuer Admin-Tab Logs

This commit is contained in:
2026-07-16 00:18:56 +02:00
parent 7e6ea9b010
commit 2b2a2dce2d
11 changed files with 213 additions and 26 deletions

View File

@@ -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;

View File

@@ -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
View 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 };

View File

@@ -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;

View File

@@ -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

View File

@@ -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 });
}
}
}

View File

@@ -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;

View File

@@ -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 });
});

View File

@@ -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 => {

View File

@@ -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 {}
}