Changelog: Was ist neu? Button + Unread-Tracking
This commit is contained in:
@@ -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(`
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user