Changelog-Modal: klickbar auf Versionierung, Admin kann Einträge anlegen/löschen

This commit is contained in:
2026-05-29 09:13:38 +02:00
parent 9b54bb700b
commit 0142d35901
3 changed files with 154 additions and 5 deletions

View File

@@ -322,6 +322,19 @@ if (poCols.length && !poCols.includes('retry'))
if (poCols.length && !poCols.includes('expire'))
db.exec("ALTER TABLE pushover_settings ADD COLUMN expire INTEGER DEFAULT NULL");
// ── Changelog ─────────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='changelog'").get()) {
db.exec(`
CREATE TABLE changelog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT NULL
)
`);
}
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='push_schedules'").get()) {
db.exec(`

View File

@@ -1,6 +1,6 @@
const express = require('express');
const db = require('../db');
const { authenticate } = require('../middleware/auth');
const { authenticate, requireAdmin } = require('../middleware/auth');
const router = express.Router();
const uid = req => req.user.id;
@@ -206,6 +206,24 @@ router.post('/board/:id/promote', authenticate, (req, res) => {
res.json(updated);
});
// ── Changelog ─────────────────────────────────────────────────────────────────
router.get('/changelog', authenticate, (req, res) => {
res.json(db.prepare('SELECT * FROM changelog ORDER BY created_at DESC').all());
});
router.post('/changelog', authenticate, requireAdmin, (req, res) => {
const { version, title, body } = req.body;
if (!version?.trim() || !title?.trim()) return res.status(400).json({ error: 'Version und Titel erforderlich' });
const r = db.prepare(`INSERT INTO changelog (version, title, body, created_at) VALUES (?,?,?,datetime('now','localtime'))`)
.run(version.trim(), title.trim(), body?.trim() || '');
res.json(db.prepare('SELECT * FROM changelog WHERE id=?').get(r.lastInsertRowid));
});
router.delete('/changelog/:id', authenticate, requireAdmin, (req, res) => {
db.prepare('DELETE FROM changelog WHERE id=?').run(req.params.id);
res.json({ ok: true });
});
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
router.get('/push-schedules', authenticate, (req, res) => {
const rows = db.prepare(`

View File

@@ -1171,13 +1171,127 @@ function PushScheduler({ toast }) {
);
}
// ── Changelog Modal ───────────────────────────────────────────────────────────
function ChangelogModal({ user, toast, onClose }) {
const isAdmin = user?.role === 'admin';
const [entries, setEntries] = useState([]);
const [form, setForm] = useState({ version:'', title:'', body:'' });
const [busy, setBusy] = useState(false);
const isMobile = window.innerWidth < 768;
useEffect(()=>{
api('/dashboard/changelog').then(setEntries).catch(()=>{});
},[]);
const add = async () => {
if (!form.version.trim() || !form.title.trim()) { toast('Version und Titel erforderlich','error'); return; }
setBusy(true);
try {
const r = await api('/dashboard/changelog',{body:form});
setEntries(p=>[r,...p]);
setForm({version:'',title:'',body:''});
toast('Eintrag hinzugefügt ✓');
} catch(e){ toast(e.message,'error'); }
setBusy(false);
};
const del = async id => {
try { await api(`/dashboard/changelog/${id}`,{method:'DELETE'}); setEntries(p=>p.filter(e=>e.id!==id)); }
catch(e){ toast(e.message,'error'); }
};
const fmtDate = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?'':d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}); };
return (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000,
display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
onClick={e=>e.target===e.currentTarget&&onClose()}>
<div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,width:'100%',maxWidth:540,
border:'1px solid rgba(255,255,255,0.12)',display:'flex',flexDirection:'column',
maxHeight:isMobile?'88vh':'82vh',overflow:'hidden'}}>
{/* Header */}
<div style={{padding:'16px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',flexShrink:0,
display:'flex',justifyContent:'space-between',alignItems:'center',position:'relative'}}>
{isMobile&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,
position:'absolute',top:8,left:'50%',transform:'translateX(-50%)'}}/>}
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700}}>📝 Changelog</div>
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}></button>
</div>
<div style={{overflowY:'auto',flex:1,padding:'0 20px'}}>
{/* Admin-Formular */}
{isAdmin && (
<div style={{padding:'14px 0',borderBottom:'1px solid rgba(255,255,255,0.08)'}}>
<div style={{...S.head,marginBottom:8}}>NEUER EINTRAG</div>
<div style={{display:'grid',gridTemplateColumns:'1fr 2fr',gap:8,marginBottom:8}}>
<input value={form.version} onChange={e=>setForm(p=>({...p,version:e.target.value}))}
placeholder="Version (z.B. 1.4.2)" style={{...S.inp,fontSize:12}}/>
<input value={form.title} onChange={e=>setForm(p=>({...p,title:e.target.value}))}
placeholder="Kurztitel" style={{...S.inp,fontSize:12}}/>
</div>
<textarea value={form.body} onChange={e=>setForm(p=>({...p,body:e.target.value}))}
placeholder="Beschreibung der Änderungen (Markdown-artig, z.B. - Neue Funktion X&#10;- Fix: Bug Y)"
rows={3} style={{...S.inp,resize:'vertical',fontSize:12,marginBottom:8,width:'100%',boxSizing:'border-box'}}/>
<button onClick={add} disabled={busy}
style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'9px 0',opacity:busy?0.5:1}}>
{busy?'…':'+ Eintrag hinzufügen'}
</button>
</div>
)}
{/* Einträge */}
<div style={{padding:'8px 0 20px'}}>
{entries.length===0 ? (
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:11,padding:'20px 0',textAlign:'center'}}>
Noch keine Changelog-Einträge.
</div>
) : entries.map(e=>(
<div key={e.id} style={{padding:'14px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
<div style={{display:'flex',alignItems:'flex-start',justifyContent:'space-between',gap:10,marginBottom:4}}>
<div style={{display:'flex',alignItems:'center',gap:8,flexWrap:'wrap'}}>
<span style={{
background:'rgba(78,205,196,0.12)',border:'1px solid rgba(78,205,196,0.25)',
borderRadius:5,padding:'2px 8px',color:'#4ecdc4',
fontFamily:'monospace',fontSize:11,fontWeight:700,flexShrink:0
}}>{e.version}</span>
<span style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{e.title}</span>
</div>
<div style={{display:'flex',alignItems:'center',gap:6,flexShrink:0}}>
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:9}}>
{fmtDate(e.created_at)}
</span>
{isAdmin && (
<button onClick={()=>del(e.id)}
style={{background:'transparent',border:'none',cursor:'pointer',
color:'rgba(255,107,157,0.5)',fontSize:13,padding:'0 2px',lineHeight:1}}></button>
)}
</div>
</div>
{e.body && (
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,
lineHeight:1.7,whiteSpace:'pre-wrap',paddingLeft:4}}>
{e.body}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
);
}
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, user }) {
const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false);
const [showBoard, setShowBoard] = useState(false);
const [showChangelog, setShowChangelog] = useState(false);
useEffect(() => { const t = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(t); }, []);
return (
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
{showBoard && <BoardModal user={user} toast={toast} onClose={()=>setShowBoard(false)} onRead={onBoardRead}/>}
{showChangelog && <ChangelogModal user={user} toast={toast} onClose={()=>setShowChangelog(false)}/>}
<div style={{ marginBottom:16, display:'flex', alignItems:'flex-start', justifyContent:'space-between' }}>
<div>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile ? 17 : 22, margin:0 }}>Dashboard</h1>
@@ -1200,10 +1314,14 @@ function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unre
<TodoList toast={toast} />
<Notepad toast={toast} />
<div style={{ marginTop:24, display:'flex', alignItems:'center', justifyContent:'center', gap:12 }}>
<div style={{ color:'rgba(255,255,255,0.35)', fontSize:9, fontFamily:'monospace', letterSpacing:1 }}>
<button onClick={()=>setShowChangelog(true)} style={{
background:'transparent', border:'none', cursor:'pointer',
color:'rgba(255,255,255,0.35)', fontSize:9, fontFamily:'monospace',
letterSpacing:1, padding:0, textDecoration:'underline dotted',
textUnderlineOffset:3 }}>
{typeof __BUILD_TIME__ !== 'undefined' && __BUILD_TIME__
? `Letzte Versionierung: ${__BUILD_TIME__}` : null}
</div>
? `Letzte Versionierung: ${__BUILD_TIME__}` : 'Changelog'}
</button>
<button onClick={()=>setShowBoard(true)} style={{
position:'relative', background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.1)', borderRadius:7,