Add update script, enhance admin features, and improve styles

- Created a PowerShell script (update.ps1) for automating the process of pulling the latest repository changes, stopping, building, and starting the Docker Compose stack.
- Updated App.jsx to include new admin sections for alarms and integrations, added state management for runner jobs, and enhanced permission checks.
- Extended the API module (api.js) with new endpoints for managing integrations and runner jobs.
- Improved CSS styles for a more compact and responsive layout in the admin interface, focusing on source and datapoint cards.
This commit is contained in:
jhartworks
2026-07-09 15:24:58 +02:00
parent f247d2827d
commit 19d27f9de9
14 changed files with 1077 additions and 54 deletions
+98
View File
@@ -132,6 +132,9 @@ const PERMISSION_CATALOG = [
{ id: "manage_users", label: "Benutzer verwalten" },
{ id: "manage_roles", label: "Rollen verwalten" },
{ id: "view_debug", label: "Treiber-Debug anzeigen" },
{ id: "manage_alarms", label: "Alarme verwalten" },
{ id: "manage_integrations", label: "API-Verbindungen verwalten" },
{ id: "manage_runner", label: "CSV-Runner verwalten" },
];
async function getUserPermissions(roleName) {
@@ -198,6 +201,51 @@ async function ensureRolesSchema() {
} catch {}
}
function sanitizeApiSettings(body = {}) {
return {
mailUrl: String(body.mailUrl || "https://mailapi.se-inno.de").trim() || "https://mailapi.se-inno.de",
mailApiKey: String(body.mailApiKey || "").trim(),
whatsappUrl: String(body.whatsappUrl || "https://whatsappapi.se-inno.de").trim() || "https://whatsappapi.se-inno.de",
whatsappApiKey: String(body.whatsappApiKey || "").trim(),
voipUrl: String(body.voipUrl || "https://voipapi.se-inno.de").trim() || "https://voipapi.se-inno.de",
voipApiKey: String(body.voipApiKey || "").trim(),
updatedAt: new Date().toISOString(),
};
}
function sanitizeRunnerJob(body = {}, existing = {}) {
const time = String(body.time ?? existing.time ?? "06:00").trim();
if (!/^\d{2}:\d{2}$/.test(time)) {
throw new Error("Uhrzeit bitte im Format HH:MM angeben.");
}
return {
...existing,
id: existing.id || body.id || `job-${Date.now()}`,
name: String(body.name ?? existing.name ?? "CSV Export").trim() || "CSV Export",
selectionId: String(body.selectionId ?? existing.selectionId ?? "").trim(),
enabled: typeof body.enabled === "boolean" ? body.enabled : Boolean(existing.enabled ?? true),
time,
rangeHours: Number(body.rangeHours ?? existing.rangeHours ?? 24) || 24,
delimiter: String(body.delimiter ?? existing.delimiter ?? ";").replace("\\t", "\t") || ";",
mailEnabled: typeof body.mailEnabled === "boolean" ? body.mailEnabled : Boolean(existing.mailEnabled),
mailTo: String(body.mailTo ?? existing.mailTo ?? "").trim(),
mailSubject: String(body.mailSubject ?? existing.mailSubject ?? "SE-LTD CSV Export").trim(),
mailMessage: String(body.mailMessage ?? existing.mailMessage ?? "Automatischer CSV Export aus SE Local Trenddata.").trim(),
updatedAt: new Date().toISOString(),
createdAt: existing.createdAt || new Date().toISOString(),
};
}
async function listRunnerJobs() {
const ids = await redis.smembers("runner:job:ids");
const jobs = await Promise.all(ids.map(async (id) => {
const raw = await redis.get(`runner:job:${id}`);
const result = await redis.get(`runner:last-result:${id}`);
return raw ? { ...JSON.parse(raw), lastResult: result ? JSON.parse(result) : null } : null;
}));
return jobs.filter(Boolean).sort((left, right) => String(left.time).localeCompare(String(right.time)));
}
async function resolveSelectionPoints(body, user) {
if (body.selectionId) {
const selection = await getSelection(redis, body.selectionId);
@@ -752,6 +800,51 @@ app.post("/api/dashboard/data", authenticateTokenOptional, wrap(async (req, res)
res.json({ widgets: results });
}));
app.get("/api/integrations", authenticateToken, requirePermission("manage_integrations"), wrap(async (_req, res) => {
const current = JSON.parse(await redis.get("integration:api") || "{}");
res.json({
...sanitizeApiSettings(current),
mailConfigured: Boolean(current.mailApiKey),
whatsappConfigured: Boolean(current.whatsappApiKey),
voipConfigured: Boolean(current.voipApiKey),
});
}));
app.put("/api/integrations", authenticateToken, requirePermission("manage_integrations"), wrap(async (req, res) => {
const payload = sanitizeApiSettings(req.body || {});
await redis.set("integration:api", JSON.stringify(payload));
res.json({ ...payload, mailConfigured: Boolean(payload.mailApiKey), whatsappConfigured: Boolean(payload.whatsappApiKey), voipConfigured: Boolean(payload.voipApiKey) });
}));
app.get("/api/runner/jobs", authenticateToken, requirePermission("manage_runner"), wrap(async (_req, res) => {
res.json({ jobs: await listRunnerJobs() });
}));
app.post("/api/runner/jobs", authenticateToken, requirePermission("manage_runner"), wrap(async (req, res) => {
const job = sanitizeRunnerJob(req.body || {});
if (!job.selectionId) return res.status(400).json({ error: "Selection ist erforderlich." });
await redis.set(`runner:job:${job.id}`, JSON.stringify(job));
await redis.sadd("runner:job:ids", job.id);
res.status(201).json(job);
}));
app.patch("/api/runner/jobs/:jobId", authenticateToken, requirePermission("manage_runner"), wrap(async (req, res) => {
const id = req.params.jobId;
const existing = JSON.parse(await redis.get(`runner:job:${id}`) || "null");
if (!existing) return res.status(404).json({ error: "Runner-Job nicht gefunden." });
const job = sanitizeRunnerJob(req.body || {}, existing);
await redis.set(`runner:job:${id}`, JSON.stringify(job));
await redis.sadd("runner:job:ids", id);
res.json(job);
}));
app.delete("/api/runner/jobs/:jobId", authenticateToken, requirePermission("manage_runner"), wrap(async (req, res) => {
const id = req.params.jobId;
await redis.del(`runner:job:${id}`, `runner:last:${id}`, `runner:last-result:${id}`);
await redis.srem("runner:job:ids", id);
res.json({ ok: true });
}));
app.get("/api/preferences", authenticateToken, wrap(async (req, res) => {
const preferences = await getPreferences(redis, req.user.sub);
res.json(preferences);
@@ -781,3 +874,8 @@ app.listen(config.port, () => {
+6 -3
View File
@@ -13,13 +13,13 @@ export const DEFAULT_ROLE_DEFINITIONS = [
{
name: "technician",
label: "Techniker",
permissions: ["view_dashboard", "view_trends", "edit_aliases", "manage_sources", "view_debug"],
permissions: ["view_dashboard", "view_trends", "edit_aliases", "manage_sources", "view_debug", "manage_alarms"],
isSystem: true,
},
{
name: "admin",
label: "Admin",
permissions: ["view_dashboard", "view_trends", "edit_aliases", "manage_sources", "manage_users", "manage_roles", "view_debug"],
permissions: ["view_dashboard", "view_trends", "edit_aliases", "manage_sources", "manage_users", "manage_roles", "view_debug", "manage_integrations", "manage_runner", "manage_alarms"],
isSystem: true,
},
];
@@ -104,7 +104,7 @@ async function ensureDefaultRoles(redis) {
...(existing || {}),
name: definition.name,
label: existing?.label || definition.label,
permissions: Array.isArray(existing?.permissions) && existing.permissions.length ? existing.permissions : definition.permissions,
permissions: Array.from(new Set([...(Array.isArray(existing?.permissions) ? existing.permissions : []), ...definition.permissions])),
isSystem: true,
});
await redis.set(key, JSON.stringify(payload));
@@ -286,3 +286,6 @@ export async function savePreferences(redis, userId, preferences) {
await redis.set(`preferences:${userId}`, JSON.stringify(payload));
return payload;
}