Push-Erinnerungen: Widget, Backend-Scheduler, Pushover-Versand jede Minute

This commit is contained in:
2026-05-28 19:00:48 +02:00
parent cc6a1c6dc7
commit 7ec2879f73
4 changed files with 204 additions and 0 deletions

View File

@@ -313,6 +313,20 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='boa
`); `);
} }
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='push_schedules'").get()) {
db.exec(`
CREATE TABLE push_schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
message TEXT NOT NULL,
scheduled_at DATETIME NOT NULL,
sent INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT NULL
)
`);
}
// ── Link-Liste ──────────────────────────────────────────────────────────────── // ── Link-Liste ────────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list'").get()) { if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list'").get()) {
db.exec(` db.exec(`

View File

@@ -68,3 +68,33 @@ app.use(express.static(PUBLIC));
app.get('*', (_req, res) => res.sendFile(path.join(PUBLIC, 'index.html'))); app.get('*', (_req, res) => res.sendFile(path.join(PUBLIC, 'index.html')));
app.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000')); 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) {
try {
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 }),
});
} catch(e) { console.error('Pushover Fehler:', e.message); }
}
setInterval(async () => {
try {
const due = db.prepare(`
SELECT s.*, p.user_key, p.app_token
FROM push_schedules s
JOIN pushover_settings p ON p.user_id = s.user_id
WHERE s.sent = 0 AND s.scheduled_at <= datetime('now','localtime')
`).all();
for (const row of due) {
await sendPushoverMsg(row.user_key, row.app_token, row.message);
db.prepare('UPDATE push_schedules SET sent=1 WHERE id=?').run(row.id);
console.log(`✓ Push gesendet an user ${row.user_id}: ${row.message}`);
}
} catch(e) { console.error('Push-Job Fehler:', e.message); }
}, 60 * 1000); // jede Minute

View File

@@ -206,4 +206,38 @@ router.post('/board/:id/promote', authenticate, (req, res) => {
res.json(updated); res.json(updated);
}); });
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
router.get('/push-schedules', authenticate, (req, res) => {
const rows = db.prepare(`
SELECT * FROM push_schedules
WHERE user_id=? AND sent=0 AND scheduled_at > datetime('now','localtime')
ORDER BY scheduled_at ASC
`).all(uid(req));
res.json(rows);
});
router.post('/push-schedules', authenticate, (req, res) => {
const { message, scheduled_at } = req.body;
if (!message?.trim() || !scheduled_at) return res.status(400).json({ error: 'Nachricht und Zeitpunkt erforderlich' });
const r = db.prepare(`
INSERT INTO push_schedules (user_id, message, scheduled_at, created_at)
VALUES (?, ?, ?, datetime('now','localtime'))
`).run(uid(req), message.trim(), scheduled_at);
res.json(db.prepare('SELECT * FROM push_schedules WHERE id=?').get(r.lastInsertRowid));
});
router.put('/push-schedules/:id', authenticate, (req, res) => {
const row = db.prepare('SELECT * FROM push_schedules WHERE id=? AND user_id=?').get(req.params.id, uid(req));
if (!row) return res.status(404).json({ error: 'Nicht gefunden' });
const { message, scheduled_at } = req.body;
db.prepare('UPDATE push_schedules SET message=?, scheduled_at=?, sent=0 WHERE id=?')
.run(message ?? row.message, scheduled_at ?? row.scheduled_at, row.id);
res.json(db.prepare('SELECT * FROM push_schedules WHERE id=?').get(row.id));
});
router.delete('/push-schedules/:id', authenticate, (req, res) => {
db.prepare('DELETE FROM push_schedules WHERE id=? AND user_id=?').run(req.params.id, uid(req));
res.json({ ok: true });
});
module.exports = router; module.exports = router;

View File

@@ -990,6 +990,131 @@ function BoardModal({ user, toast, onClose, onRead }) {
} }
// ── Push-Zeitplaner Widget ────────────────────────────────────────────────────
function PushScheduler({ toast }) {
const [isOpen, setIsOpen] = useState(false);
const [schedules, setSchedules] = useState([]);
const [msg, setMsg] = useState('');
const [dt, setDt] = useState('');
const [editId, setEditId] = useState(null);
const [editMsg, setEditMsg] = useState('');
const [editDt, setEditDt] = useState('');
const [hasPushover,setHasPushover]= useState(null); // null=unbekannt
useEffect(() => {
if (!isOpen) return;
api('/dashboard/push-schedules').then(setSchedules).catch(()=>{});
if (hasPushover === null)
api('/tools/nachrichten/pushover').then(d=>setHasPushover(!!(d.user_key&&d.app_token))).catch(()=>{});
}, [isOpen]);
// Vorauswahl: jetzt + 1 Stunde
const defaultDt = () => {
const d = new Date(Date.now() + 60*60*1000);
return d.toISOString().slice(0,16);
};
const fmtDt = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?s:d.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); };
const add = async () => {
if (!msg.trim()) { toast('Nachricht eingeben','error'); return; }
if (!dt) { toast('Datum und Uhrzeit wählen','error'); return; }
try {
const r = await api('/dashboard/push-schedules',{body:{message:msg.trim(),scheduled_at:dt}});
setSchedules(p=>[...p,r].sort((a,b)=>a.scheduled_at.localeCompare(b.scheduled_at)));
setMsg(''); setDt(''); toast('Erinnerung gespeichert ✓');
} catch(e){ toast(e.message,'error'); }
};
const del = async id => {
try { await api(`/dashboard/push-schedules/${id}`,{method:'DELETE'}); setSchedules(p=>p.filter(s=>s.id!==id)); }
catch(e){ toast(e.message,'error'); }
};
const saveEdit = async () => {
try {
const r = await api(`/dashboard/push-schedules/${editId}`,{method:'PUT',body:{message:editMsg,scheduled_at:editDt}});
setSchedules(p=>p.map(s=>s.id===editId?r:s).sort((a,b)=>a.scheduled_at.localeCompare(b.scheduled_at)));
setEditId(null); toast('Gespeichert ✓');
} catch(e){ toast(e.message,'error'); }
};
return (
<div style={{...S.card, marginBottom:10}}>
<button onClick={()=>setIsOpen(v=>!v)} style={{
width:'100%',background:'transparent',border:'none',cursor:'pointer',
display:'flex',alignItems:'center',padding:0,gap:8}}>
<span style={{fontSize:13}}>🔔</span>
<span style={{...S.head,marginBottom:0,flex:1,textAlign:'left'}}>PUSH-ERINNERUNGEN</span>
{schedules.length>0&&!isOpen&&(
<span style={{color:'rgba(78,205,196,0.7)',fontFamily:'monospace',fontSize:9}}>{schedules.length} aktiv</span>
)}
<span style={{color:'rgba(255,255,255,0.4)',fontSize:11,
transform:isOpen?'rotate(90deg)':'rotate(0)',transition:'transform 0.2s'}}></span>
</button>
{isOpen && (
<div style={{marginTop:12}}>
{hasPushover===false && (
<div style={{background:'rgba(255,230,109,0.07)',border:'1px solid rgba(255,230,109,0.2)',
borderRadius:7,padding:'8px 10px',color:'#ffe66d',fontFamily:'monospace',fontSize:10,
marginBottom:10,lineHeight:1.6}}>
Kein Pushover eingerichtet. Bitte erst unter Einstellungen Profil Pushover konfigurieren.
</div>
)}
{/* Neue Erinnerung */}
<div style={{marginBottom:14}}>
<input value={msg} onChange={e=>setMsg(e.target.value)}
placeholder="Nachricht…" style={{...S.inp,marginBottom:6,fontSize:13}}
onKeyDown={e=>e.key==='Enter'&&add()}/>
<div style={{display:'flex',gap:6}}>
<input type="datetime-local" value={dt||defaultDt()}
onChange={e=>setDt(e.target.value)}
style={{...S.inp,flex:1,fontSize:12,colorScheme:'dark'}}/>
<button onClick={add} style={{...S.btn('#4ecdc4'),padding:'0 14px',flexShrink:0}}>+</button>
</div>
</div>
{/* Liste */}
{schedules.length===0 ? (
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:11}}>Keine aktiven Erinnerungen.</div>
) : schedules.map(s=>(
<div key={s.id} style={{padding:'8px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
{editId===s.id ? (
<div>
<input value={editMsg} onChange={e=>setEditMsg(e.target.value)}
style={{...S.inp,marginBottom:6,fontSize:13}} autoFocus/>
<div style={{display:'flex',gap:6}}>
<input type="datetime-local" value={editDt} onChange={e=>setEditDt(e.target.value)}
style={{...S.inp,flex:1,fontSize:12,colorScheme:'dark'}}/>
<button onClick={saveEdit} style={{...S.btn('#4ecdc4'),padding:'0 10px'}}></button>
<button onClick={()=>setEditId(null)} style={{...S.btn('#ff6b9d',true),padding:'0 10px'}}></button>
</div>
</div>
) : (
<div style={{display:'flex',alignItems:'center',gap:8}}>
<div style={{flex:1,minWidth:0}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:12,
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{s.message}</div>
<div style={{color:'#4ecdc4',fontFamily:'monospace',fontSize:10,marginTop:2}}>
🕐 {fmtDt(s.scheduled_at)}
</div>
</div>
<button onClick={()=>{setEditId(s.id);setEditMsg(s.message);setEditDt(s.scheduled_at?.slice(0,16)||'');}}
style={{...S.btn('#4ecdc4',true),padding:'3px 8px',fontSize:11,flexShrink:0}}></button>
<button onClick={()=>del(s.id)}
style={{...S.btn('#ff6b9d',true),padding:'3px 8px',fontSize:11,flexShrink:0}}></button>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, user }) { function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, user }) {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false); const [showBoard, setShowBoard] = useState(false);
@@ -1013,6 +1138,7 @@ function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unre
fontFamily:'monospace', marginTop:2, flexShrink:0 }}></button> fontFamily:'monospace', marginTop:2, flexShrink:0 }}></button>
</div> </div>
<QuickLinks toast={toast} mobile={mobile} /> <QuickLinks toast={toast} mobile={mobile} />
<PushScheduler toast={toast}/>
<CalendarWidget/> <CalendarWidget/>
<BestellStats/> <BestellStats/>
<TodoList toast={toast} /> <TodoList toast={toast} />