feat: HA-Entitaeten fuer Push-Erinnerungen und Kanban; neues Feature Sidebar-Sichtbarkeit pro Nutzer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user