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'))
|
if (clCols.length && !clCols.includes('build_time'))
|
||||||
db.exec("ALTER TABLE changelog ADD COLUMN build_time TEXT DEFAULT NULL");
|
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 ───────────────────────────────────────────────────────────
|
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
|
||||||
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='push_schedules'").get()) {
|
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='push_schedules'").get()) {
|
||||||
db.exec(`
|
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());
|
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) => {
|
router.post('/changelog', authenticate, requireAdmin, (req, res) => {
|
||||||
const { version, title, body, build_time } = req.body;
|
const { version, title, body, build_time } = req.body;
|
||||||
if (!version?.trim() || !title?.trim()) return res.status(400).json({ error: 'Version und Titel erforderlich' });
|
if (!version?.trim() || !title?.trim()) return res.status(400).json({ error: 'Version und Titel erforderlich' });
|
||||||
|
|||||||
@@ -1172,7 +1172,7 @@ function PushScheduler({ toast }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Changelog Modal ───────────────────────────────────────────────────────────
|
// ── Changelog Modal ───────────────────────────────────────────────────────────
|
||||||
function ChangelogModal({ user, toast, onClose }) {
|
function ChangelogModal({ user, toast, onClose, onRead }) {
|
||||||
const isAdmin = user?.role === 'admin';
|
const isAdmin = user?.role === 'admin';
|
||||||
const [entries, setEntries] = useState([]);
|
const [entries, setEntries] = useState([]);
|
||||||
const [form, setForm] = useState({ version:'', title:'', body:'' });
|
const [form, setForm] = useState({ version:'', title:'', body:'' });
|
||||||
@@ -1181,6 +1181,7 @@ function ChangelogModal({ user, toast, onClose }) {
|
|||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
api('/dashboard/changelog').then(setEntries).catch(()=>{});
|
api('/dashboard/changelog').then(setEntries).catch(()=>{});
|
||||||
|
api('/dashboard/changelog/read',{method:'POST'}).then(()=>{ if(onRead) onRead(); }).catch(()=>{});
|
||||||
},[]);
|
},[]);
|
||||||
|
|
||||||
const add = async () => {
|
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 [now, setNow] = useState(new Date());
|
||||||
const [showBoard, setShowBoard] = useState(false);
|
const [showBoard, setShowBoard] = useState(false);
|
||||||
const [showChangelog, setShowChangelog] = useState(false);
|
const [showChangelog, setShowChangelog] = useState(false);
|
||||||
@@ -1294,10 +1295,21 @@ function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unre
|
|||||||
return (
|
return (
|
||||||
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
||||||
{showBoard && <BoardModal user={user} toast={toast} onClose={()=>setShowBoard(false)} onRead={onBoardRead}/>}
|
{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 style={{ marginBottom:16, display:'flex', alignItems:'flex-start', justifyContent:'space-between' }}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile ? 17 : 22, margin:0 }}>Dashboard</h1>
|
<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 }}>
|
<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' })}
|
{now.toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
|
||||||
{' · '}
|
{' · '}
|
||||||
@@ -2158,6 +2170,7 @@ export default function App() {
|
|||||||
const [unreadMsgs, setUnreadMsgs] = useState(0);
|
const [unreadMsgs, setUnreadMsgs] = useState(0);
|
||||||
const [unreadBoard, setUnreadBoard] = useState(0);
|
const [unreadBoard, setUnreadBoard] = useState(0);
|
||||||
const [unreadPromoted, setUnreadPromoted] = useState(0);
|
const [unreadPromoted, setUnreadPromoted] = useState(0);
|
||||||
|
const [unreadChangelog,setUnreadChangelog]= useState(0);
|
||||||
|
|
||||||
const [authChecked, setAuthChecked] = useState(false);
|
const [authChecked, setAuthChecked] = useState(false);
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
@@ -2198,6 +2211,14 @@ export default function App() {
|
|||||||
return()=>clearInterval(t);
|
return()=>clearInterval(t);
|
||||||
},[user]);
|
},[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(()=>{
|
useEffect(()=>{
|
||||||
if(!user)return;
|
if(!user)return;
|
||||||
const poll=()=>api('/tools/nachrichten/unread').then(d=>setUnreadMsgs(d.count||0)).catch(()=>{});
|
const poll=()=>api('/tools/nachrichten/unread').then(d=>setUnreadMsgs(d.count||0)).catch(()=>{});
|
||||||
@@ -2245,7 +2266,7 @@ export default function App() {
|
|||||||
unreadMsgs={unreadMsgs}/>
|
unreadMsgs={unreadMsgs}/>
|
||||||
)}
|
)}
|
||||||
<main style={mainStyle}>
|
<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}/>}
|
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
|
||||||
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive}/>}
|
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive}/>}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user