From 9b54bb700b53de83a9a06c69d648242add642d81 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 29 May 2026 09:00:10 +0200 Subject: [PATCH] =?UTF-8?q?Pushover:=20Emergency-Priorit=C3=A4t=20mit=20Re?= =?UTF-8?q?try,=20has=5Fpushover=20Badge=20in=20Admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/db.js | 7 +++ backend/src/index.js | 15 +++-- backend/src/routes/admin.js | 8 ++- backend/src/tools/nachrichten/routes.js | 35 +++++++---- frontend/src/App.jsx | 84 ++++++++++++++++++++++--- 5 files changed, 123 insertions(+), 26 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index 9a3718c..30ade24 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -315,6 +315,13 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='boa `); } +// Migration: Pushover retry/expire +const poCols = db.prepare("PRAGMA table_info(pushover_settings)").all().map(r => r.name); +if (poCols.length && !poCols.includes('retry')) + db.exec("ALTER TABLE pushover_settings ADD COLUMN retry INTEGER DEFAULT NULL"); +if (poCols.length && !poCols.includes('expire')) + db.exec("ALTER TABLE pushover_settings ADD COLUMN expire INTEGER DEFAULT NULL"); + // ── Push-Zeitplaner ─────────────────────────────────────────────────────────── if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='push_schedules'").get()) { db.exec(` diff --git a/backend/src/index.js b/backend/src/index.js index f327729..38ff8de 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -89,12 +89,18 @@ app.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000')); // ── Push-Zeitplaner Hintergrund-Job ────────────────────────────────────────── const db = require('./db'); -async function sendPushoverMsg(userKey, appToken, message) { +async function sendPushoverMsg(userKey, appToken, message, opts = {}) { try { + const params = { token: appToken, user: userKey, title: 'DickenDock Erinnerung', message }; + if (opts.retry && opts.expire) { + params.priority = 2; + params.retry = opts.retry; + params.expire = opts.expire; + } await fetch('https://api.pushover.net/1/messages.json', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ token: appToken, user: userKey, title: 'DickenDock Erinnerung', message }), + body: new URLSearchParams(params), }); } catch(e) { console.error('Pushover Fehler:', e.message); } } @@ -110,13 +116,14 @@ async function runScheduler() { try { const now = db.prepare("SELECT datetime('now','localtime') as t").get().t; const due = db.prepare(` - SELECT s.*, p.user_key, p.app_token + SELECT s.*, p.user_key, p.app_token, p.retry, p.expire FROM push_schedules s JOIN pushover_settings p ON p.user_id = s.user_id WHERE s.sent = 0 AND s.scheduled_at <= ? `).all(now); for (const row of due) { - await sendPushoverMsg(row.user_key, row.app_token, row.message); + await sendPushoverMsg(row.user_key, row.app_token, row.message, + { retry: row.retry, expire: row.expire }); 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/routes/admin.js b/backend/src/routes/admin.js index ed84ef0..aa88ff3 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -31,7 +31,13 @@ router.post('/restore', authenticate, requireAdmin, upload.single('database'), ( // GET /api/admin/users – alle Benutzer mit Statistiken router.get('/users', authenticate, requireAdmin, (req, res) => { - const users = db.prepare("SELECT id, username, role, created_at, last_active_at FROM users ORDER BY role DESC, username").all(); + const users = db.prepare(` + SELECT u.id, u.username, u.role, u.created_at, u.last_active_at, + CASE WHEN p.user_id IS NOT NULL THEN 1 ELSE 0 END as has_pushover + FROM users u + LEFT JOIN pushover_settings p ON p.user_id = u.id + ORDER BY u.role DESC, u.username + `).all(); const stats = users.map(u => { const archiv = db.prepare('SELECT COUNT(*) c FROM calculations WHERE user_id=?').get(u.id).c; const bestellung = db.prepare('SELECT COUNT(*) c FROM orders WHERE user_id=?').get(u.id).c; diff --git a/backend/src/tools/nachrichten/routes.js b/backend/src/tools/nachrichten/routes.js index e1b5e16..b084363 100644 --- a/backend/src/tools/nachrichten/routes.js +++ b/backend/src/tools/nachrichten/routes.js @@ -4,12 +4,18 @@ const { authenticate } = require('../../middleware/auth'); const router = express.Router(); // ── Pushover Helper ─────────────────────────────────────────────────────────── -async function sendPushover(userKey, appToken, title, message) { +async function sendPushover(userKey, appToken, title, message, opts = {}) { try { + const params = { token: appToken, user: userKey, title, message }; + if (opts.retry && opts.expire) { + params.priority = 2; + params.retry = opts.retry; + params.expire = opts.expire; + } await fetch('https://api.pushover.net/1/messages.json', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ token: appToken, user: userKey, title, message }), + body: new URLSearchParams(params), }); } catch {} } @@ -96,7 +102,8 @@ router.post('/messages/:userId', authenticate, async (req, res) => { const senderName = db.prepare('SELECT username FROM users WHERE id = ?').get(me)?.username || 'Jemand'; const pushover = db.prepare('SELECT * FROM pushover_settings WHERE user_id = ?').get(recipId); if (pushover) { - await sendPushover(pushover.user_key, pushover.app_token, 'DickenDock', `Neue Nachricht von ${senderName}`); + await sendPushover(pushover.user_key, pushover.app_token, 'DickenDock', `Neue Nachricht von ${senderName}`, + { retry: pushover.retry, expire: pushover.expire }); } res.json({ id: result.lastInsertRowid }); @@ -132,18 +139,19 @@ router.delete('/my-messages', authenticate, (req, res) => { // ── Pushover Settings ───────────────────────────────────────────────────────── router.get('/pushover', authenticate, (req, res) => { - const row = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id = ?').get(req.user.id); - res.json(row || { user_key: '', app_token: '' }); + const row = db.prepare('SELECT user_key, app_token, retry, expire FROM pushover_settings WHERE user_id = ?').get(req.user.id); + res.json(row || { user_key: '', app_token: '', retry: null, expire: null }); }); router.post('/pushover', authenticate, (req, res) => { - const { user_key, app_token } = req.body; + const { user_key, app_token, retry, expire } = req.body; if (!user_key || !app_token) return res.status(400).json({ error: 'Beide Felder erforderlich' }); db.prepare(` - INSERT INTO pushover_settings (user_id, user_key, app_token) - VALUES (?, ?, ?) - ON CONFLICT(user_id) DO UPDATE SET user_key=excluded.user_key, app_token=excluded.app_token - `).run(req.user.id, user_key, app_token); + INSERT INTO pushover_settings (user_id, user_key, app_token, retry, expire) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(user_id) DO UPDATE SET user_key=excluded.user_key, app_token=excluded.app_token, + retry=excluded.retry, expire=excluded.expire + `).run(req.user.id, user_key, app_token, retry || null, expire || null); res.json({ ok: true }); }); @@ -155,7 +163,12 @@ router.delete('/pushover', authenticate, (req, res) => { router.post('/pushover/test', authenticate, async (req, res) => { const row = db.prepare('SELECT * FROM pushover_settings WHERE user_id = ?').get(req.user.id); if (!row) return res.status(400).json({ error: 'Keine Pushover-Einstellungen gespeichert' }); - await sendPushover(row.user_key, row.app_token, 'DickenDock', 'Pushover-Verbindung erfolgreich! 🎉'); + const useEmergency = req.body?.emergency === true && row.retry && row.expire; + const msg = useEmergency + ? `🚨 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 } : {}); res.json({ ok: true }); }); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index be3bbf5..ac07756 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -757,30 +757,46 @@ function NachrichtenWidget({ unreadMsgs, setActive }) { // ── Pushover Settings ───────────────────────────────────────────────────────── function PushoverSettings({ toast }) { - const [form, setForm] = useState({ user_key:'', app_token:'' }); + const [form, setForm] = useState({ user_key:'', app_token:'', retry: '', expire: '' }); const [busy, setBusy] = useState(false); const [hasSaved, setHasSaved] = useState(false); + const [emergency,setEmergency]= useState(false); useEffect(() => { api('/tools/nachrichten/pushover') .then(d => { - const v = { user_key: d.user_key||'', app_token: d.app_token||'' }; + const v = { user_key: d.user_key||'', app_token: d.app_token||'', + retry: d.retry||'', expire: d.expire||'' }; setForm(v); setHasSaved(!!(v.user_key && v.app_token)); + setEmergency(!!(d.retry && d.expire)); }).catch(()=>{}); }, []); const save = async () => { if (!form.user_key.trim() || !form.app_token.trim()) { toast('Beide Felder ausfüllen', 'error'); return; } setBusy(true); - try { await api('/tools/nachrichten/pushover', { body: form }); toast('Gespeichert'); setHasSaved(true); } - catch(e) { toast(e.message, 'error'); } + try { + await api('/tools/nachrichten/pushover', { body: { + ...form, + retry: emergency ? (parseInt(form.retry) || 60) : null, + expire: emergency ? (parseInt(form.expire) || 3600) : null, + }}); + toast('Gespeichert'); setHasSaved(true); + } catch(e) { toast(e.message, 'error'); } setBusy(false); }; const test = async () => { setBusy(true); - try { await api('/tools/nachrichten/pushover/test', { method:'POST' }); toast('Testbenachrichtigung gesendet ✓'); } + try { await api('/tools/nachrichten/pushover/test', { method:'POST', body:{ emergency: false } }); toast('Test gesendet ✓ (normale Priorität)'); } + catch(e) { toast(e.message, 'error'); } + setBusy(false); + }; + + const testEmergency = async () => { + setBusy(true); + try { await api('/tools/nachrichten/pushover/test', { method:'POST', body:{ emergency: true } }); toast('🚨 Emergency-Test gesendet – quittiere in der Pushover-App!'); } catch(e) { toast(e.message, 'error'); } setBusy(false); }; @@ -788,8 +804,8 @@ function PushoverSettings({ toast }) { const clear = async () => { try { await api('/tools/nachrichten/pushover', { method:'DELETE' }); - setForm({ user_key:'', app_token:'' }); - setHasSaved(false); + setForm({ user_key:'', app_token:'', retry:'', expire:'' }); + setHasSaved(false); setEmergency(false); toast('Pushover entfernt'); } catch(e) { toast(e.message, 'error'); } }; @@ -797,12 +813,47 @@ function PushoverSettings({ toast }) { return (

- Push-Benachrichtigungen bei neuen Nachrichten via{' '} + Push-Benachrichtigungen via{' '} pushover.net. Du brauchst einen Account und eine eigene App (kostenlos).

setForm(p=>({...p,user_key:e.target.value}))} /> setForm(p=>({...p,app_token:e.target.value}))} /> + + {/* Emergency / Wiederholung */} +
+ + {emergency && ( +
+
+ + setForm(p=>({...p,retry:e.target.value}))} + style={{ ...S.inp, fontSize:13 }}/> +
+ min. 30s · z.B. 60 = jede Minute +
+
+
+ + setForm(p=>({...p,expire:e.target.value}))} + style={{ ...S.inp, fontSize:13 }}/> +
+ z.B. 3600 = max. 1 Stunde +
+
+
+ )} +
+
{hasSaved && ( + style={{ ...S.btn('#ffe66d', true), padding:'0 10px', opacity:busy?0.5:1, fontSize:10 }}>Test + )} + {hasSaved && emergency && ( + )} {hasSaved && ( + style={{ ...S.btn('#ff6b9d', true), padding:'0 10px' }}>✕ )}
@@ -1499,6 +1554,15 @@ function UserManagement({ toast }) { border:`1px solid ${u.role==='admin'?'rgba(255,230,109,0.3)':'rgba(78,205,196,0.3)'}`, borderRadius:4, padding:'1px 6px', }}>{u.role} + {!!u.has_pushover && ( + 🔔 Push + )} {u.last_active_at && (