Changelog-Modal: klickbar auf Versionierung, Admin kann Einträge anlegen/löschen
This commit is contained in:
@@ -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(`
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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 - 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,
|
||||
|
||||
Reference in New Issue
Block a user