feat: HA-Entitaeten fuer Push-Erinnerungen und Kanban; neues Feature Sidebar-Sichtbarkeit pro Nutzer

This commit is contained in:
2026-07-10 20:47:57 +02:00
parent 37ebad91c6
commit d372bed860
6 changed files with 185 additions and 11 deletions

View File

@@ -258,6 +258,8 @@ if (!userCols2.includes('hidden'))
db.exec("ALTER TABLE users ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0");
if (!userCols2.includes('chat_active_at'))
db.exec("ALTER TABLE users ADD COLUMN chat_active_at DATETIME DEFAULT NULL");
if (!userCols2.includes('hidden_tools'))
db.exec("ALTER TABLE users ADD COLUMN hidden_tools TEXT NOT NULL DEFAULT '[]'");
// ── Login-Sicherheit ──────────────────────────────────────────────────────────
// Spalten für Account-Lockout in users-Tabelle

View File

@@ -302,6 +302,8 @@ async function runScheduler() {
mqttClient.publishPresence();
mqttClient.publishMediaAnfragen();
mqttClient.publishDruckStatistik();
mqttClient.publishPushErinnerungen();
mqttClient.publishKanban();
try {
const now = db.prepare("SELECT datetime('now','localtime') as t").get().t;
const due = db.prepare(`

View File

@@ -288,6 +288,28 @@ function publishDiscovery() {
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
}],
// ── Dashboard ─────────────────────────────────────────────────────────
['sensor', 'dickendock_dashboard_push_erinnerungen', {
name: 'Dashboard Push-Erinnerungen offen',
unique_id: 'dickendock_dashboard_push_erinnerungen',
state_topic: `${BASE_TOPIC}/dashboard/push_erinnerungen`,
json_attributes_topic: `${BASE_TOPIC}/dashboard/push_erinnerungen_attributes`,
icon: 'mdi:bell-ring-outline',
unit_of_measurement: 'Erinnerungen', state_class: 'measurement',
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
}],
// ── Kanban ────────────────────────────────────────────────────────────
['sensor', 'dickendock_kanban_karten', {
name: 'Kanban Karten offen',
unique_id: 'dickendock_kanban_karten',
state_topic: `${BASE_TOPIC}/kanban/karten`,
json_attributes_topic: `${BASE_TOPIC}/kanban/karten_attributes`,
icon: 'mdi:view-column-outline',
unit_of_measurement: 'Karten', state_class: 'measurement',
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
}],
// ── System ────────────────────────────────────────────────────────────
['sensor', 'dickendock_system_online', {
name: 'System Nutzer online',
@@ -542,6 +564,43 @@ function publishDruckStatistik() {
pub('druck/bestellungen_gesamt', allOrders.length);
}
// Wird jede Minute vom Scheduler aufgerufen — offene (noch nicht versendete,
// in der Zukunft liegende) Push-Erinnerungen aus dem Dashboard, systemweit
function publishPushErinnerungen() {
if (!client?.connected) return;
const offen = db.prepare(`
SELECT s.message, s.scheduled_at, u.username
FROM push_schedules s JOIN users u ON u.id = s.user_id
WHERE s.sent = 0 AND s.scheduled_at > datetime('now','localtime')
ORDER BY s.scheduled_at ASC
`).all();
client.publish(`${BASE_TOPIC}/dashboard/push_erinnerungen`, String(offen.length), { retain: true });
client.publish(`${BASE_TOPIC}/dashboard/push_erinnerungen_attributes`, JSON.stringify({
erinnerungen: offen,
}), { retain: true });
}
// Wird jede Minute vom Scheduler aufgerufen — alle Kanban-Karten über alle
// Nutzer-Boards hinweg (Kanban ist pro Nutzer, hier systemweit aggregiert)
function publishKanban() {
if (!client?.connected) return;
const karten = db.prepare(`
SELECT k.title, k.priority, col.title AS spalte, u.username
FROM kanban_cards k
JOIN kanban_columns col ON col.id = k.column_id
JOIN users u ON u.id = k.user_id
ORDER BY u.username ASC, col.position ASC, k.position ASC
`).all();
const proNutzer = {};
for (const k of karten) proNutzer[k.username] = (proNutzer[k.username] || 0) + 1;
client.publish(`${BASE_TOPIC}/kanban/karten`, String(karten.length), { retain: true });
client.publish(`${BASE_TOPIC}/kanban/karten_attributes`, JSON.stringify({
karten,
pro_nutzer: proNutzer,
}), { retain: true });
}
// Wird von index.js gesetzt, um Zirkel-Requires zu vermeiden
function setKoepiCheckHandler(fn) { koepiCheckHandler = fn; }
function setMediaAckHandler(fn) { mediaAckHandler = fn; }
@@ -553,6 +612,8 @@ module.exports = {
publishPresence,
publishMediaAnfragen,
publishDruckStatistik,
publishPushErinnerungen,
publishKanban,
setKoepiCheckHandler,
setMediaAckHandler,
setMediaAckAllHandler,

View File

@@ -32,7 +32,7 @@ router.post('/restore', authenticate, requireAdmin, upload.single('database'), (
// GET /api/admin/users alle Benutzer mit Statistiken
router.get('/users', authenticate, requireAdmin, (req, res) => {
const users = db.prepare(`
SELECT u.id, u.username, u.role, u.created_at, u.last_active_at, u.hidden,
SELECT u.id, u.username, u.role, u.created_at, u.last_active_at, u.hidden, u.hidden_tools,
CASE WHEN p.user_id IS NOT NULL THEN 1 ELSE 0 END as has_pushover
FROM users u
LEFT JOIN pushover_settings p ON p.user_id = u.id
@@ -47,7 +47,9 @@ router.get('/users', authenticate, requireAdmin, (req, res) => {
const files = db.prepare('SELECT COUNT(*) c FROM files WHERE user_id=?').get(u.id).c;
const noteLen = (db.prepare('SELECT content FROM notes WHERE user_id=?').get(u.id)?.content || '').length;
const icals = db.prepare('SELECT COUNT(*) c FROM calendar_feeds WHERE user_id=?').get(u.id).c;
return { ...u, archiv, bestellung, snippets_c, todos, links, files, icals, noteLen };
let hidden_tools = [];
try { hidden_tools = JSON.parse(u.hidden_tools || '[]'); } catch {}
return { ...u, hidden_tools, archiv, bestellung, snippets_c, todos, links, files, icals, noteLen };
});
res.json(stats);
});
@@ -170,6 +172,18 @@ router.put('/users/:id/hidden', authenticate, requireAdmin, (req, res) => {
res.json({ hidden: newHidden });
});
// PUT /api/admin/users/:id/hidden-tools Sidebar-Sichtbarkeit für einen User setzen
// body: { hidden_tools: ['schocken', 'paywallkiller', ...] } (tool-ids aus toolRegistry.js)
router.put('/users/:id/hidden-tools', authenticate, requireAdmin, (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id=?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'Nicht gefunden' });
const { hidden_tools } = req.body;
if (!Array.isArray(hidden_tools)) return res.status(400).json({ error: 'hidden_tools muss ein Array sein' });
const cleaned = [...new Set(hidden_tools.filter(t => typeof t === 'string'))];
db.prepare('UPDATE users SET hidden_tools=? WHERE id=?').run(JSON.stringify(cleaned), user.id);
res.json({ hidden_tools: cleaned });
});
// Test-Pushover an beliebigen User senden
router.post('/users/:id/test-push', authenticate, requireAdmin, async (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id=?').get(req.params.id);

View File

@@ -166,4 +166,14 @@ router.put('/preferences', authenticate, (req, res) => {
res.json(merged);
});
// ── GET /api/auth/hidden-tools ────────────────────────────────────────────────
// Liefert die vom Admin für diesen Nutzer ausgeblendeten Sidebar-Bereiche.
// Bewusst NICHT über /preferences (das ist selbst-editierbar) — Sichtbarkeit
// darf nur der Admin ändern.
router.get('/hidden-tools', authenticate, (req, res) => {
const user = db.prepare('SELECT hidden_tools FROM users WHERE id=?').get(req.user.id);
try { res.json(JSON.parse(user?.hidden_tools || '[]')); }
catch { res.json([]); }
});
module.exports = router;