diff --git a/backend/src/tools/whiteboard/routes.js b/backend/src/tools/whiteboard/routes.js
index 1f11370..6f9540d 100644
--- a/backend/src/tools/whiteboard/routes.js
+++ b/backend/src/tools/whiteboard/routes.js
@@ -25,11 +25,41 @@ const router = express.Router();
role TEXT NOT NULL DEFAULT 'edit',
UNIQUE(whiteboard_id, user_id)
);
+ CREATE TABLE IF NOT EXISTS whiteboard_unread (
+ whiteboard_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ reason TEXT NOT NULL DEFAULT 'updated',
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
+ PRIMARY KEY (whiteboard_id, user_id)
+ );
`);
})();
const uid = req => req.user.id;
+function getAccessUsers(whiteboardId, exceptUserId) {
+ const wb = db.prepare('SELECT owner_id FROM whiteboards WHERE id=?').get(whiteboardId);
+ if (!wb) return [];
+ const users = new Set();
+ if (wb.owner_id !== exceptUserId) users.add(wb.owner_id);
+ const perms = db.prepare('SELECT user_id FROM whiteboard_permissions WHERE whiteboard_id=?').all(whiteboardId);
+ for (const p of perms) if (p.user_id !== exceptUserId) users.add(p.user_id);
+ return [...users];
+}
+
+function markUnread(whiteboardId, userIds, reason = 'updated') {
+ const upsert = db.prepare(`
+ INSERT INTO whiteboard_unread (whiteboard_id, user_id, reason, created_at)
+ VALUES (?, ?, ?, datetime('now','localtime'))
+ ON CONFLICT(whiteboard_id, user_id) DO UPDATE SET reason=excluded.reason, created_at=excluded.created_at
+ `);
+ for (const u of userIds) upsert.run(whiteboardId, u, reason);
+}
+
+function markRead(whiteboardId, userId) {
+ db.prepare('DELETE FROM whiteboard_unread WHERE whiteboard_id=? AND user_id=?').run(whiteboardId, userId);
+}
+
// Hilfsfunktion: Hat User Zugriff? Gibt 'owner'|'edit'|'view'|null zurück
function access(whiteboardId, userId) {
const wb = db.prepare('SELECT owner_id FROM whiteboards WHERE id=?').get(whiteboardId);
@@ -39,6 +69,13 @@ function access(whiteboardId, userId) {
return perm?.role || null;
}
+// ── Unread count ──────────────────────────────────────────────────────────────
+router.get('/unread', authenticate, (req, res) => {
+ const me = uid(req);
+ const { count } = db.prepare('SELECT COUNT(*) as count FROM whiteboard_unread WHERE user_id=?').get(me);
+ res.json({ count });
+});
+
// ── Liste ─────────────────────────────────────────────────────────────────────
router.get('/', authenticate, (req, res) => {
const me = uid(req);
@@ -100,6 +137,7 @@ router.get('/:id/data', authenticate, (req, res) => {
JOIN users u ON u.id=p.user_id WHERE p.whiteboard_id=?
`).all(req.params.id);
const owner = db.prepare('SELECT username FROM users WHERE id=?').get(wb.owner_id);
+ markRead(req.params.id, me);
res.json({ ...data, role, title: wb.title, owner: owner.username, permissions: perms });
});
@@ -123,12 +161,10 @@ router.post('/:id/save', authenticate, async (req, res) => {
const message = `${saver?.username || 'Jemand'} hat das Whiteboard "${wb?.title || ''}" gespeichert.`;
// Alle User mit Zugriff: Owner + alle Permissions — außer dem Speichernden selbst
- const notify = [];
- if (wb && wb.owner_id !== me) notify.push(wb.owner_id);
- const perms = db.prepare('SELECT user_id FROM whiteboard_permissions WHERE whiteboard_id=?').all(req.params.id);
- for (const p of perms) { if (p.user_id !== me) notify.push(p.user_id); }
+ const notify = getAccessUsers(req.params.id, me);
+ markUnread(req.params.id, notify, 'updated');
- for (const userId of [...new Set(notify)]) {
+ for (const userId of notify) {
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(userId);
if (!poCfg?.app_token || !poCfg?.user_key) continue;
fetch('https://api.pushover.net/1/messages.json', {
@@ -175,9 +211,10 @@ router.put('/:id/permissions', authenticate, async (req, res) => {
}
})();
- // Pushover an neu hinzugekommene User (fire & forget)
const owner = db.prepare('SELECT username FROM users WHERE id=?').get(me);
const newlyAdded = permissions.filter(p => p.role && p.role !== 'none' && !before.includes(p.user_id));
+ if (newlyAdded.length) markUnread(req.params.id, newlyAdded.map(p => p.user_id), 'shared');
+
for (const p of newlyAdded) {
try {
const poCfg = db.prepare('SELECT user_key, app_token FROM pushover_settings WHERE user_id=?').get(p.user_id);
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index a13f0b8..ffed280 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -155,7 +155,7 @@ function Login({ onLogin }) {
// ── Update Modal ──────────────────────────────────────────────────────────────
// ── Desktop Sidebar ───────────────────────────────────────────────────────────
-function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0 }) {
+function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0, whiteboardUnread=0 }) {
const [appsOpen, setAppsOpen] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const [collapsedGroups, setCollapsedGroups] = useState({});
@@ -186,6 +186,10 @@ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns
)}
+ {collapsed && item.id==='whiteboard' && whiteboardUnread>0 && (
+
+ )}
{!collapsed && {item.label}}
{!collapsed && item.id==='nachrichten' && unreadMsgs>0 && (
@@ -196,6 +200,10 @@ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns
{hexWarsTurns}
)}
+ {!collapsed && item.id==='whiteboard' && whiteboardUnread>0 && (
+ {whiteboardUnread}
+ )}
);
};
@@ -288,7 +296,7 @@ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns
}
// ── Mobile Bottom Navigation ──────────────────────────────────────────────────
-function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0 }) {
+function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0, whiteboardUnread=0 }) {
const [menuOpen, setMenuOpen] = useState(false);
const [appsOpen, setAppsOpen] = useState(false);
const [mobileCollapsedGroups, setMobileCollapsedGroups] = useState({});
@@ -365,7 +373,7 @@ function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTur
{!isGrpCollapsed && tools.map(t => (
0}
+ label={t.label} dot={(t.id==='gebietseroberung' && hexWarsTurns>0) || (t.id==='whiteboard' && whiteboardUnread>0)}
onClick={() => { setActive(t.id); setAppsOpen(false); }} />
))}
@@ -399,7 +407,7 @@ function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTur
style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center',
justifyContent:'center', gap:3, background:'transparent', border:'none',
cursor:'pointer', position:'relative', minWidth:0, padding:'0 4px' }}>
- {isApps && (hexWarsTurns>0 || unreadMsgs>0) && (
+ {isApps && (hexWarsTurns>0 || unreadMsgs>0 || whiteboardUnread>0) && (
@@ -3707,7 +3715,8 @@ export default function App() {
};
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
const [unreadMsgs, setUnreadMsgs] = useState(0);
- const [hexWarsTurns, setHexWarsTurns] = useState(0);
+ const [hexWarsTurns, setHexWarsTurns] = useState(0);
+ const [whiteboardUnread, setWhiteboardUnread] = useState(0);
const [unreadBoard, setUnreadBoard] = useState(0);
const [unreadPromoted, setUnreadPromoted] = useState(0);
const [unreadChangelog,setUnreadChangelog]= useState(0);
@@ -3779,6 +3788,14 @@ export default function App() {
return()=>clearInterval(t);
},[user]);
+ useEffect(()=>{
+ if(!user)return;
+ const poll=()=>api('/tools/whiteboard/unread').then(d=>setWhiteboardUnread(d.count||0)).catch(()=>{});
+ poll();
+ const t=setInterval(poll,30*1000);
+ return()=>clearInterval(t);
+ },[user]);
+
const toast=useCallback((msg,type='success')=>{
setToastMsg({msg,type});
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
@@ -3816,7 +3833,7 @@ export default function App() {
{localStorage.removeItem('sk_token');setUser(null);}}
- unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns}/>
+ unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns} whiteboardUnread={whiteboardUnread}/>
)}
{active==='dashboard' && { setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} unackedFavs={unackedFavs} onClearUnackedFavs={()=>setUnackedFavs({count:0,isAdmin:false})} user={user}/>}
@@ -3828,7 +3845,7 @@ export default function App() {
{localStorage.removeItem('sk_token');setUser(null);}}
- unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns}/>
+ unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns} whiteboardUnread={whiteboardUnread}/>
)}