Pushover: Emergency-Priorität mit Retry, has_pushover Badge in Admin
This commit is contained in:
@@ -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(`
|
||||
|
||||
@@ -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})`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<p style={{ color:'rgba(255,255,255,0.42)', fontSize:11, fontFamily:'monospace', marginBottom:12, lineHeight:1.7 }}>
|
||||
Push-Benachrichtigungen bei neuen Nachrichten via{' '}
|
||||
Push-Benachrichtigungen via{' '}
|
||||
<a href="https://pushover.net" target="_blank" rel="noopener noreferrer" style={{ color:'#4ecdc4' }}>pushover.net</a>.
|
||||
Du brauchst einen Account und eine eigene App (kostenlos).
|
||||
</p>
|
||||
<Field label="USER KEY" value={form.user_key} onChange={e=>setForm(p=>({...p,user_key:e.target.value}))} />
|
||||
<Field label="APP TOKEN" value={form.app_token} onChange={e=>setForm(p=>({...p,app_token:e.target.value}))} />
|
||||
|
||||
{/* Emergency / Wiederholung */}
|
||||
<div style={{ marginBottom:14, background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius:8, padding:'10px 12px' }}>
|
||||
<label style={{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', marginBottom: emergency ? 10 : 0 }}>
|
||||
<input type="checkbox" checked={emergency} onChange={e=>setEmergency(e.target.checked)}
|
||||
style={{ width:14, height:14, accentColor:'#ff6b9d', cursor:'pointer' }}/>
|
||||
<span style={{ color:'rgba(255,255,255,0.7)', fontFamily:'monospace', fontSize:11 }}>
|
||||
🚨 Wiederholen bis Quittierung (Emergency-Priorität)
|
||||
</span>
|
||||
</label>
|
||||
{emergency && (
|
||||
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8, marginTop:6 }}>
|
||||
<div>
|
||||
<label style={{ ...S.head, display:'block', marginBottom:3, fontSize:9 }}>WIEDERHOLUNG (SEK)</label>
|
||||
<input type="number" value={form.retry||60} min={30} max={3600}
|
||||
onChange={e=>setForm(p=>({...p,retry:e.target.value}))}
|
||||
style={{ ...S.inp, fontSize:13 }}/>
|
||||
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:8, marginTop:2 }}>
|
||||
min. 30s · z.B. 60 = jede Minute
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ ...S.head, display:'block', marginBottom:3, fontSize:9 }}>MAX. DAUER (SEK)</label>
|
||||
<input type="number" value={form.expire||3600} min={60} max={86400}
|
||||
onChange={e=>setForm(p=>({...p,expire:e.target.value}))}
|
||||
style={{ ...S.inp, fontSize:13 }}/>
|
||||
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:8, marginTop:2 }}>
|
||||
z.B. 3600 = max. 1 Stunde
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display:'flex', gap:8 }}>
|
||||
<button onClick={save} disabled={busy}
|
||||
style={{ ...S.btn('#4ecdc4'), flex:1, textAlign:'center', padding:'10px 0', opacity:busy?0.5:1 }}>
|
||||
@@ -810,11 +861,15 @@ function PushoverSettings({ toast }) {
|
||||
</button>
|
||||
{hasSaved && (
|
||||
<button onClick={test} disabled={busy}
|
||||
style={{ ...S.btn('#ffe66d', true), padding:'0 14px', opacity:busy?0.5:1 }}>Test</button>
|
||||
style={{ ...S.btn('#ffe66d', true), padding:'0 10px', opacity:busy?0.5:1, fontSize:10 }}>Test</button>
|
||||
)}
|
||||
{hasSaved && emergency && (
|
||||
<button onClick={testEmergency} disabled={busy}
|
||||
style={{ ...S.btn('#ff6b9d', true), padding:'0 10px', opacity:busy?0.5:1, fontSize:10 }}>🚨 Test</button>
|
||||
)}
|
||||
{hasSaved && (
|
||||
<button onClick={clear}
|
||||
style={{ ...S.btn('#ff6b9d', true), padding:'0 14px' }}>✕</button>
|
||||
style={{ ...S.btn('#ff6b9d', true), padding:'0 10px' }}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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}</span>
|
||||
{!!u.has_pushover && (
|
||||
<span title="Pushover eingerichtet" style={{
|
||||
marginLeft:4, fontSize:9, fontFamily:'monospace',
|
||||
color:'rgba(255,107,157,0.8)',
|
||||
background:'rgba(255,107,157,0.1)',
|
||||
border:'1px solid rgba(255,107,157,0.25)',
|
||||
borderRadius:4, padding:'1px 6px',
|
||||
}}>🔔 Push</span>
|
||||
)}
|
||||
{u.last_active_at && (
|
||||
<span style={{
|
||||
marginLeft:6, fontSize:9, fontFamily:'monospace',
|
||||
|
||||
Reference in New Issue
Block a user