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
+57 -17
View File
@@ -120,16 +120,19 @@ function getBroadcastTargets(routing, body = {}) {
if (!isAutoBroadcast(configured)) {
targets.push(configured);
}
for (const subnet of parseHostList(body.scanSubnets || body.bacnetScanSubnets || body.options?.bacnetScanSubnets)) {
const configuredSubnets = parseHostList(body.scanSubnets || body.bacnetScanSubnets || body.options?.bacnetScanSubnets);
for (const subnet of configuredSubnets) {
const broadcast = cidrBroadcast(subnet);
if (broadcast) targets.push(broadcast);
}
for (const candidate of [body.clientHost, body.browserHost, body.locationHost]) {
const broadcast = broadcastFromAddress(candidate);
if (broadcast) targets.push(broadcast);
if (!configuredSubnets.length) {
for (const candidate of [body.clientHost, body.browserHost, body.locationHost]) {
const broadcast = broadcastFromAddress(candidate);
if (broadcast) targets.push(broadcast);
}
targets.push(...getInterfaceBroadcasts());
targets.push(...parseHostList(process.env.BACNET_BROADCAST_FALLBACKS));
}
targets.push(...getInterfaceBroadcasts());
targets.push(...parseHostList(process.env.BACNET_BROADCAST_FALLBACKS));
if (!targets.length) {
targets.push("255.255.255.255");
}
@@ -413,6 +416,20 @@ function extractValue(propertyValue) {
return propertyValue?.values?.[0]?.value ?? propertyValue?.values?.[0] ?? null;
}
function bacnetUnitSymbol(value) {
const unit = Number(value);
const map = {
3: "A",
5: "V",
53: "Pa",
62: "°C",
63: "K",
95: "",
98: "%",
};
return Object.prototype.hasOwnProperty.call(map, unit) ? map[unit] : "";
}
async function scanObjects(client, device, portNumber) {
const host = device.address;
const deviceObject = { type: 8, instance: Number(device.deviceId) };
@@ -429,6 +446,7 @@ async function scanObjects(client, device, portNumber) {
const objectName = await readProperty(client, host, objectId, 77);
const description = await readProperty(client, host, objectId, 28);
const presentValue = await readProperty(client, host, objectId, 85);
const units = await readProperty(client, host, objectId, 117);
const raw = extractValue(presentValue);
const name = normalizeText(extractValue(objectName)) || objectLabel(objectId);
const alias = normalizeText(extractValue(description)) || name;
@@ -438,6 +456,7 @@ async function scanObjects(client, device, portNumber) {
host,
port: portNumber,
deviceId: device.deviceId,
deviceName: device.name || "BACnet Gerät " + device.deviceId,
objectType: objectId.type,
objectInstance: objectId.instance,
propertyId: 85,
@@ -447,6 +466,7 @@ async function scanObjects(client, device, portNumber) {
description: alias,
alias,
kind: typeof raw === "boolean" || (objectId.type >= 3 && objectId.type <= 5) ? "digital" : "analog",
unit: typeof raw === "boolean" || (objectId.type >= 3 && objectId.type <= 5) ? "bool" : bacnetUnitSymbol(extractValue(units)),
currentValue: raw,
hasValue: raw !== null,
});
@@ -646,15 +666,25 @@ function isLikelyDockerGatewayAddress(address) {
function preferRealBacnetAddresses(devices) {
const list = Array.isArray(devices) ? devices : [];
const idsWithRealAddress = new Set(
list
.filter((device) => !isLikelyDockerGatewayAddress(device.address || device.host))
.map((device) => String(device.deviceId)),
);
return list.filter((device) => {
const address = device.address || device.host;
return !(idsWithRealAddress.has(String(device.deviceId)) && isLikelyDockerGatewayAddress(address));
});
const byDeviceId = new Map();
for (const device of list) {
const id = String(device?.deviceId || "");
if (!id) continue;
const address = device.address || device.host || "";
const current = byDeviceId.get(id);
if (!current) {
byDeviceId.set(id, device);
continue;
}
const currentAddress = current.address || current.host || "";
const deviceIsReal = !isLikelyDockerGatewayAddress(address);
const currentIsReal = !isLikelyDockerGatewayAddress(currentAddress);
const hasMorePoints = Number(device.pointCount || device.points?.length || 0) > Number(current.pointCount || current.points?.length || 0);
if ((deviceIsReal && !currentIsReal) || (deviceIsReal === currentIsReal && hasMorePoints)) {
byDeviceId.set(id, { ...current, ...device, points: device.points?.length ? device.points : current.points });
}
}
return Array.from(byDeviceId.values());
}
async function scanNetwork(body) {
const timeout = Number(body.timeout || defaultTimeout);
@@ -706,7 +736,7 @@ async function scanNetwork(body) {
await waitForSocket(client);
await registerForeignDevice(client, routing);
sendWhoIs(client, routing, body.deviceId);
await sleep(Math.min(timeout, 1800));
await sleep(Math.min(timeout, Number(body.initialWaitMs || process.env.BACNET_INITIAL_WAIT_MS || 4500)));
const hosts = routing.mode === "broadcast" ? buildUnicastScanHosts(body, routing) : [];
const needsUnicastMapping = routing.mode === "broadcast" && hosts.length && (!devices.length || devices.some((device) => !hosts.includes(device.address)));
@@ -721,7 +751,7 @@ async function scanNetwork(body) {
}
}
currentUnicastTarget = "";
await sleep(250);
await sleep(Number(body.finalWaitMs || process.env.BACNET_FINAL_WAIT_MS || 1200));
}
let visibleDevices = preferRealBacnetAddresses(devices);
@@ -864,3 +894,13 @@ server.listen(port, () => {
});