Changelog: Was ist neu? Button + Unread-Tracking

This commit is contained in:
2026-05-29 10:09:47 +02:00
parent b91b742dd8
commit b1e2bfdeec
3 changed files with 50 additions and 5 deletions

View File

@@ -342,6 +342,15 @@ const clCols = db.prepare("PRAGMA table_info(changelog)").all().map(r => r.name)
if (clCols.length && !clCols.includes('build_time'))
db.exec("ALTER TABLE changelog ADD COLUMN build_time TEXT DEFAULT NULL");
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='changelog_reads'").get()) {
db.exec(`
CREATE TABLE changelog_reads (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
last_read 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

@@ -211,6 +211,21 @@ router.get('/changelog', authenticate, (req, res) => {
res.json(db.prepare('SELECT * FROM changelog ORDER BY created_at DESC').all());
});
router.get('/changelog/unread', authenticate, (req, res) => {
const u = req.user.id;
const lastRead = db.prepare('SELECT last_read FROM changelog_reads WHERE user_id=?').get(u)?.last_read;
const count = lastRead
? db.prepare('SELECT COUNT(*) c FROM changelog WHERE created_at > ?').get(lastRead).c
: db.prepare('SELECT COUNT(*) c FROM changelog').get().c;
res.json({ unread: count });
});
router.post('/changelog/read', authenticate, (req, res) => {
db.prepare(`INSERT OR REPLACE INTO changelog_reads (user_id, last_read) VALUES (?, datetime('now','localtime'))`)
.run(req.user.id);
res.json({ ok: true });
});
router.post('/changelog', authenticate, requireAdmin, (req, res) => {
const { version, title, body, build_time } = req.body;
if (!version?.trim() || !title?.trim()) return res.status(400).json({ error: 'Version und Titel erforderlich' });

View File

@@ -1172,7 +1172,7 @@ function PushScheduler({ toast }) {
}
// ── Changelog Modal ───────────────────────────────────────────────────────────
function ChangelogModal({ user, toast, onClose }) {
function ChangelogModal({ user, toast, onClose, onRead }) {
const isAdmin = user?.role === 'admin';
const [entries, setEntries] = useState([]);
const [form, setForm] = useState({ version:'', title:'', body:'' });
@@ -1181,6 +1181,7 @@ function ChangelogModal({ user, toast, onClose }) {
useEffect(()=>{
api('/dashboard/changelog').then(setEntries).catch(()=>{});
api('/dashboard/changelog/read',{method:'POST'}).then(()=>{ if(onRead) onRead(); }).catch(()=>{});
},[]);
const add = async () => {
@@ -1286,7 +1287,7 @@ function ChangelogModal({ user, toast, onClose }) {
);
}
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, unreadChangelog=0, onChangelogRead, user }) {
const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false);
const [showChangelog, setShowChangelog] = useState(false);
@@ -1294,10 +1295,21 @@ function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unre
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)}/>}
{showChangelog && <ChangelogModal user={user} toast={toast} onClose={()=>setShowChangelog(false)} onRead={onChangelogRead}/>}
<div style={{ marginBottom:16, display:'flex', alignItems:'flex-start', justifyContent:'space-between' }}>
<div>
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile ? 17 : 22, margin:0 }}>Dashboard</h1>
{unreadChangelog > 0 && (
<button onClick={()=>setShowChangelog(true)} style={{
background:'linear-gradient(135deg,rgba(78,205,196,0.15),rgba(78,205,196,0.05))',
border:'1px solid rgba(78,205,196,0.3)', borderRadius:20,
color:'#4ecdc4', cursor:'pointer', fontFamily:'monospace',
fontSize:10, padding:'3px 10px', letterSpacing:0.5,
animation:'pulse 2s infinite',
}}> Was ist neu?</button>
)}
</div>
<p style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginTop:3 }}>
{now.toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
{' · '}
@@ -2158,6 +2170,7 @@ export default function App() {
const [unreadMsgs, setUnreadMsgs] = useState(0);
const [unreadBoard, setUnreadBoard] = useState(0);
const [unreadPromoted, setUnreadPromoted] = useState(0);
const [unreadChangelog,setUnreadChangelog]= useState(0);
const [authChecked, setAuthChecked] = useState(false);
useEffect(()=>{
@@ -2198,6 +2211,14 @@ export default function App() {
return()=>clearInterval(t);
},[user]);
useEffect(()=>{
if(!user)return;
const poll=()=>api('/dashboard/changelog/unread').then(d=>setUnreadChangelog(d.unread||0)).catch(()=>{});
poll();
const t=setInterval(poll,60*1000);
return()=>clearInterval(t);
},[user]);
useEffect(()=>{
if(!user)return;
const poll=()=>api('/tools/nachrichten/unread').then(d=>setUnreadMsgs(d.count||0)).catch(()=>{});
@@ -2245,7 +2266,7 @@ export default function App() {
unreadMsgs={unreadMsgs}/>
)}
<main style={mainStyle}>
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} user={user}/>}
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>}
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive}/>}
</main>