node-red-contrib-alarm-ultimate 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/examples/README.md +80 -3
- package/examples/alarm-ultimate-dashboard-controls.json +31 -2
- package/examples/alarm-ultimate-dashboard-v2.json +74 -2
- package/examples/alarm-ultimate-dashboard.json +31 -2
- package/examples/alarm-ultimate-home-assistant-alarm-panel.json +335 -0
- package/nodes/AlarmSystemUltimate.html +166 -28
- package/nodes/AlarmSystemUltimate.js +119 -4
- package/package.json +1 -1
- package/test/alarm-system.spec.js +61 -0
- package/tools/alarm-json-mapper.html +55 -36
- package/tools/alarm-panel.html +390 -33
package/tools/alarm-panel.html
CHANGED
|
@@ -66,6 +66,15 @@
|
|
|
66
66
|
display: none;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
body.view-keypad #logCard,
|
|
70
|
+
body.view-zones #logCard {
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
body.view-log .grid {
|
|
75
|
+
display: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
header {
|
|
70
79
|
padding: 16px;
|
|
71
80
|
border-bottom: 1px solid var(--border);
|
|
@@ -354,6 +363,11 @@
|
|
|
354
363
|
font-size: 12px;
|
|
355
364
|
margin: 8px 0 0 0;
|
|
356
365
|
}
|
|
366
|
+
|
|
367
|
+
.log-details {
|
|
368
|
+
color: var(--muted);
|
|
369
|
+
font-size: 11px;
|
|
370
|
+
}
|
|
357
371
|
</style>
|
|
358
372
|
</head>
|
|
359
373
|
<body>
|
|
@@ -437,6 +451,32 @@
|
|
|
437
451
|
</div>
|
|
438
452
|
</section>
|
|
439
453
|
</div>
|
|
454
|
+
|
|
455
|
+
<section class="card" id="logCard" data-section="log">
|
|
456
|
+
<h2>Log</h2>
|
|
457
|
+
<div class="hint" id="logHint">Loading...</div>
|
|
458
|
+
<div class="buttons" style="margin-top:8px;">
|
|
459
|
+
<button id="btnLogFilterAll" type="button">ALL</button>
|
|
460
|
+
<button id="btnLogFilterAlarm" type="button">ALARM</button>
|
|
461
|
+
<button id="btnLogFilterArming" type="button">ARMING</button>
|
|
462
|
+
<button id="btnLogFilterZones" type="button">ZONES</button>
|
|
463
|
+
<button id="btnLogFilterErrors" type="button">ERRORS</button>
|
|
464
|
+
<button id="btnLogDownload" type="button">Download JSON</button>
|
|
465
|
+
</div>
|
|
466
|
+
<div style="overflow:auto; margin-top:10px;">
|
|
467
|
+
<table>
|
|
468
|
+
<thead>
|
|
469
|
+
<tr>
|
|
470
|
+
<th>Alarm</th>
|
|
471
|
+
<th>Time</th>
|
|
472
|
+
<th>Event</th>
|
|
473
|
+
<th>Details</th>
|
|
474
|
+
</tr>
|
|
475
|
+
</thead>
|
|
476
|
+
<tbody id="logBody"></tbody>
|
|
477
|
+
</table>
|
|
478
|
+
</div>
|
|
479
|
+
</section>
|
|
440
480
|
</main>
|
|
441
481
|
|
|
442
482
|
<script>
|
|
@@ -453,6 +493,14 @@
|
|
|
453
493
|
zonesHint: document.getElementById("zonesHint"),
|
|
454
494
|
btnZonesFilterOpen: document.getElementById("btnZonesFilterOpen"),
|
|
455
495
|
btnZonesFilterAll: document.getElementById("btnZonesFilterAll"),
|
|
496
|
+
logBody: document.getElementById("logBody"),
|
|
497
|
+
logHint: document.getElementById("logHint"),
|
|
498
|
+
btnLogFilterAll: document.getElementById("btnLogFilterAll"),
|
|
499
|
+
btnLogFilterAlarm: document.getElementById("btnLogFilterAlarm"),
|
|
500
|
+
btnLogFilterArming: document.getElementById("btnLogFilterArming"),
|
|
501
|
+
btnLogFilterZones: document.getElementById("btnLogFilterZones"),
|
|
502
|
+
btnLogFilterErrors: document.getElementById("btnLogFilterErrors"),
|
|
503
|
+
btnLogDownload: document.getElementById("btnLogDownload"),
|
|
456
504
|
code: document.getElementById("code"),
|
|
457
505
|
cmdStatus: document.getElementById("cmdStatus"),
|
|
458
506
|
btnArm: document.getElementById("btnArm"),
|
|
@@ -472,14 +520,22 @@
|
|
|
472
520
|
document.body.classList.add("view-zones");
|
|
473
521
|
} else if (view === "keypad") {
|
|
474
522
|
document.body.classList.add("view-keypad");
|
|
523
|
+
} else if (view === "log") {
|
|
524
|
+
document.body.classList.add("view-log");
|
|
475
525
|
}
|
|
476
526
|
|
|
527
|
+
const showLog = view !== "zones" && view !== "keypad";
|
|
528
|
+
|
|
477
529
|
let nodesList = [];
|
|
478
530
|
let selectedIds = new Set();
|
|
479
531
|
let pollTimer = null;
|
|
480
532
|
let nodeStateTimer = null;
|
|
481
533
|
let nodeStateById = new Map();
|
|
482
534
|
|
|
535
|
+
let logFilter = "all"; // all | alarm | arming | zones | errors
|
|
536
|
+
let logsById = new Map(); // alarmId -> entries[]
|
|
537
|
+
let logLastTsById = new Map(); // alarmId -> last ts
|
|
538
|
+
|
|
483
539
|
let audioCtx = null;
|
|
484
540
|
let armingBeepTimer = null;
|
|
485
541
|
let hasSeenState = false;
|
|
@@ -791,6 +847,260 @@
|
|
|
791
847
|
.replace(/'/g, "'");
|
|
792
848
|
}
|
|
793
849
|
|
|
850
|
+
function pad2(n) {
|
|
851
|
+
return String(Math.max(0, Math.trunc(Number(n) || 0))).padStart(2, "0");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function formatTs(ts) {
|
|
855
|
+
const t = Number(ts) || 0;
|
|
856
|
+
if (!t) return "";
|
|
857
|
+
const d = new Date(t);
|
|
858
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(
|
|
859
|
+
d.getMinutes(),
|
|
860
|
+
)}:${pad2(d.getSeconds())}`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function logGroupForEvent(e) {
|
|
864
|
+
const evt = String(e || "").toLowerCase();
|
|
865
|
+
if (evt === "alarm") return "alarm";
|
|
866
|
+
if (evt === "error" || evt === "denied") return "errors";
|
|
867
|
+
if (
|
|
868
|
+
evt === "arming" ||
|
|
869
|
+
evt === "armed" ||
|
|
870
|
+
evt === "disarmed" ||
|
|
871
|
+
evt === "entry_delay" ||
|
|
872
|
+
evt === "arm_blocked" ||
|
|
873
|
+
evt === "already_armed" ||
|
|
874
|
+
evt === "reset" ||
|
|
875
|
+
evt === "siren_on" ||
|
|
876
|
+
evt === "siren_off"
|
|
877
|
+
)
|
|
878
|
+
return "arming";
|
|
879
|
+
if (
|
|
880
|
+
evt === "bypassed" ||
|
|
881
|
+
evt === "unbypassed" ||
|
|
882
|
+
evt === "chime" ||
|
|
883
|
+
evt === "zone_open" ||
|
|
884
|
+
evt === "zone_close" ||
|
|
885
|
+
evt === "zone_ignored_exit" ||
|
|
886
|
+
evt === "zone_bypassed_trigger" ||
|
|
887
|
+
evt === "zone_restore"
|
|
888
|
+
)
|
|
889
|
+
return "zones";
|
|
890
|
+
return "all";
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function eventPillClass(evt) {
|
|
894
|
+
const g = logGroupForEvent(evt);
|
|
895
|
+
if (g === "alarm") return "danger";
|
|
896
|
+
if (g === "errors") return "danger";
|
|
897
|
+
if (g === "arming") return "warn";
|
|
898
|
+
if (g === "zones") return "";
|
|
899
|
+
return "";
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function eventDetails(entry) {
|
|
903
|
+
const e = entry && entry.event ? String(entry.event) : "";
|
|
904
|
+
|
|
905
|
+
if (e === "alarm") {
|
|
906
|
+
const k = entry && entry.kind ? String(entry.kind) : "";
|
|
907
|
+
const z = entry && entry.zone && typeof entry.zone === "object" ? entry.zone : null;
|
|
908
|
+
const zn = z && (z.name || z.id) ? String(z.name || z.id) : "";
|
|
909
|
+
const silent = entry && entry.silent === true ? "silent" : "";
|
|
910
|
+
return [k ? `kind=${k}` : "", zn ? `zone=${zn}` : "", silent].filter(Boolean).join(" • ");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (e === "denied") {
|
|
914
|
+
const a = entry && entry.action ? String(entry.action) : "";
|
|
915
|
+
const t = entry && entry.target ? String(entry.target) : "";
|
|
916
|
+
return [a ? `action=${a}` : "", t ? `target=${t}` : ""].filter(Boolean).join(" • ");
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (e === "error") {
|
|
920
|
+
const err = entry && entry.error ? String(entry.error) : "";
|
|
921
|
+
const z = entry && entry.zone ? String(entry.zone) : "";
|
|
922
|
+
return [err ? `error=${err}` : "", z ? `zone=${z}` : ""].filter(Boolean).join(" • ");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (e === "arm_blocked") {
|
|
926
|
+
const v = Array.isArray(entry && entry.violations) ? entry.violations : [];
|
|
927
|
+
if (!v.length) return "violations";
|
|
928
|
+
const top = v
|
|
929
|
+
.slice(0, 3)
|
|
930
|
+
.map((x) => (x && typeof x === "object" ? x.id || x.name || x.zone || "" : ""))
|
|
931
|
+
.filter(Boolean)
|
|
932
|
+
.join(", ");
|
|
933
|
+
return `violations=${v.length}${top ? ` (${top}${v.length > 3 ? ", …" : ""})` : ""}`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (e === "arming" || e === "entry_delay") {
|
|
937
|
+
const s = Number.isFinite(Number(entry && entry.seconds)) ? Number(entry.seconds) : null;
|
|
938
|
+
const z = entry && entry.zone && typeof entry.zone === "object" ? entry.zone : null;
|
|
939
|
+
const zn = z && (z.name || z.id) ? String(z.name || z.id) : "";
|
|
940
|
+
return [s !== null ? `seconds=${s}` : "", zn ? `zone=${zn}` : ""].filter(Boolean).join(" • ");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (e === "armed" || e === "disarmed") {
|
|
944
|
+
const r = entry && entry.reason ? String(entry.reason) : "";
|
|
945
|
+
const d = entry && entry.duress === true ? "duress" : "";
|
|
946
|
+
return [r ? `reason=${r}` : "", d].filter(Boolean).join(" • ");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (e === "siren_on" || e === "siren_off") {
|
|
950
|
+
const r = entry && entry.reason ? String(entry.reason) : "";
|
|
951
|
+
return r ? `reason=${r}` : "";
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (
|
|
955
|
+
e === "bypassed" ||
|
|
956
|
+
e === "unbypassed" ||
|
|
957
|
+
e === "chime" ||
|
|
958
|
+
e === "zone_open" ||
|
|
959
|
+
e === "zone_close" ||
|
|
960
|
+
e === "zone_ignored_exit" ||
|
|
961
|
+
e === "zone_bypassed_trigger" ||
|
|
962
|
+
e === "zone_restore"
|
|
963
|
+
) {
|
|
964
|
+
const z = entry && entry.zone && typeof entry.zone === "object" ? entry.zone : null;
|
|
965
|
+
const zn = z && (z.name || z.id) ? String(z.name || z.id) : "";
|
|
966
|
+
const b = entry && entry.bypassed === true ? "bypassed" : "";
|
|
967
|
+
return [zn ? `zone=${zn}` : "", b].filter(Boolean).join(" • ");
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
const copy = { ...(entry || {}) };
|
|
972
|
+
delete copy.ts;
|
|
973
|
+
delete copy.event;
|
|
974
|
+
const keys = Object.keys(copy);
|
|
975
|
+
if (!keys.length) return "";
|
|
976
|
+
const txt = JSON.stringify(copy);
|
|
977
|
+
return txt.length > 220 ? `${txt.slice(0, 220)}…` : txt;
|
|
978
|
+
} catch (_err) {
|
|
979
|
+
return "";
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function updateLogFilterButtons() {
|
|
984
|
+
const map = [
|
|
985
|
+
["all", els.btnLogFilterAll],
|
|
986
|
+
["alarm", els.btnLogFilterAlarm],
|
|
987
|
+
["arming", els.btnLogFilterArming],
|
|
988
|
+
["zones", els.btnLogFilterZones],
|
|
989
|
+
["errors", els.btnLogFilterErrors],
|
|
990
|
+
];
|
|
991
|
+
map.forEach(([key, el]) => {
|
|
992
|
+
if (!el) return;
|
|
993
|
+
el.classList.toggle("selected", logFilter === key);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function setLogFilter(next) {
|
|
998
|
+
const v = String(next || "").toLowerCase();
|
|
999
|
+
logFilter = ["all", "alarm", "arming", "zones", "errors"].includes(v) ? v : "all";
|
|
1000
|
+
updateLogFilterButtons();
|
|
1001
|
+
renderLogs();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function resetLogTracking() {
|
|
1005
|
+
logsById = new Map();
|
|
1006
|
+
logLastTsById = new Map();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function fetchLogForNode(nodeId, since) {
|
|
1010
|
+
const qs = new URLSearchParams();
|
|
1011
|
+
qs.set("limit", "200");
|
|
1012
|
+
if (Number.isFinite(Number(since)) && Number(since) > 0) {
|
|
1013
|
+
qs.set("since", String(Number(since)));
|
|
1014
|
+
}
|
|
1015
|
+
const res = await fetch(apiUrl(`/alarm-ultimate/alarm/${encodeURIComponent(nodeId)}/log?${qs.toString()}`), {
|
|
1016
|
+
credentials: "same-origin",
|
|
1017
|
+
headers: { ...authHeaders() },
|
|
1018
|
+
});
|
|
1019
|
+
if (!res.ok) throw new Error(`Unable to load log (${res.status})`);
|
|
1020
|
+
return res.json();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function loadLogs(okStates) {
|
|
1024
|
+
if (!showLog) return;
|
|
1025
|
+
if (!els.logBody || !els.logHint) return;
|
|
1026
|
+
const ids = Array.from(selectedIds);
|
|
1027
|
+
if (!ids.length) return;
|
|
1028
|
+
|
|
1029
|
+
const nameById = new Map(
|
|
1030
|
+
(Array.isArray(okStates) ? okStates : []).map((x) => [x.id, (x.data && x.data.name) || x.id]),
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
const results = await Promise.allSettled(
|
|
1034
|
+
ids.map(async (id) => {
|
|
1035
|
+
const since = logLastTsById.has(id) ? logLastTsById.get(id) : null;
|
|
1036
|
+
const data = await fetchLogForNode(id, since);
|
|
1037
|
+
return { id, data };
|
|
1038
|
+
}),
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
for (const r of results) {
|
|
1042
|
+
if (r.status !== "fulfilled") continue;
|
|
1043
|
+
const id = r.value.id;
|
|
1044
|
+
const payload = r.value.data && typeof r.value.data === "object" ? r.value.data : {};
|
|
1045
|
+
const entries = Array.isArray(payload.log) ? payload.log : [];
|
|
1046
|
+
|
|
1047
|
+
const prev = logsById.get(id) || [];
|
|
1048
|
+
const next = logLastTsById.has(id) ? prev.concat(entries) : entries;
|
|
1049
|
+
logsById.set(id, next);
|
|
1050
|
+
|
|
1051
|
+
const maxTs = next.reduce((m, e) => Math.max(m, Number(e && e.ts) || 0), 0);
|
|
1052
|
+
logLastTsById.set(id, maxTs);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
renderLogs(nameById);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function renderLogs(nameById) {
|
|
1059
|
+
if (!els.logBody || !els.logHint) return;
|
|
1060
|
+
const ids = Array.from(selectedIds);
|
|
1061
|
+
const nameMap = nameById instanceof Map ? nameById : new Map(ids.map((id) => [id, id]));
|
|
1062
|
+
|
|
1063
|
+
const all = [];
|
|
1064
|
+
ids.forEach((id) => {
|
|
1065
|
+
const alarmName = nameMap.get(id) || id;
|
|
1066
|
+
const list = logsById.get(id) || [];
|
|
1067
|
+
list.forEach((e) => all.push({ ...(e || {}), __alarmId: id, __alarmName: alarmName }));
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const filtered =
|
|
1071
|
+
logFilter === "all" ? all : all.filter((e) => logGroupForEvent(e && e.event) === logFilter);
|
|
1072
|
+
|
|
1073
|
+
filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0));
|
|
1074
|
+
const show = filtered.slice(0, 200);
|
|
1075
|
+
|
|
1076
|
+
if (!show.length) {
|
|
1077
|
+
els.logHint.textContent = "No log entries yet.";
|
|
1078
|
+
els.logBody.innerHTML = "";
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
els.logHint.textContent = `Showing ${show.length} entr${show.length === 1 ? "y" : "ies"}${
|
|
1083
|
+
logFilter !== "all" ? ` (${logFilter})` : ""
|
|
1084
|
+
}.`;
|
|
1085
|
+
els.logBody.innerHTML = "";
|
|
1086
|
+
|
|
1087
|
+
for (const entry of show) {
|
|
1088
|
+
const tr = document.createElement("tr");
|
|
1089
|
+
const alarmLabel = entry.__alarmName || entry.__alarmId || "";
|
|
1090
|
+
const evt = entry && entry.event ? String(entry.event) : "";
|
|
1091
|
+
const cls = eventPillClass(evt);
|
|
1092
|
+
const details = eventDetails(entry);
|
|
1093
|
+
|
|
1094
|
+
tr.innerHTML = `
|
|
1095
|
+
<td>${escapeHtml(alarmLabel)}</td>
|
|
1096
|
+
<td>${escapeHtml(formatTs(entry.ts))}</td>
|
|
1097
|
+
<td><span class="pill small ${cls}">${escapeHtml(evt)}</span></td>
|
|
1098
|
+
<td class="log-details">${escapeHtml(details)}</td>
|
|
1099
|
+
`;
|
|
1100
|
+
els.logBody.appendChild(tr);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
794
1104
|
function normalizePreselectIds(raw) {
|
|
795
1105
|
const v = String(raw || "").trim();
|
|
796
1106
|
if (!v) return [];
|
|
@@ -841,19 +1151,20 @@
|
|
|
841
1151
|
els.selectionHint.innerHTML = `Selected: <span class="pill small">${count}</span> (click buttons to add/remove)`;
|
|
842
1152
|
}
|
|
843
1153
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1154
|
+
function setSelectedIds(next) {
|
|
1155
|
+
const ids = Array.isArray(next) ? next.map(String).filter(Boolean) : [];
|
|
1156
|
+
selectedIds = new Set(ids);
|
|
1157
|
+
// enforce non-empty selection when possible
|
|
1158
|
+
if (selectedIds.size === 0 && nodesList[0]) {
|
|
1159
|
+
selectedIds.add(nodesList[0].id);
|
|
1160
|
+
}
|
|
1161
|
+
persistSelectedIds();
|
|
1162
|
+
resetStateAudioTracking();
|
|
1163
|
+
resetLogTracking();
|
|
1164
|
+
updateSelectionHint();
|
|
1165
|
+
renderNodeButtons();
|
|
1166
|
+
loadState().catch(() => {});
|
|
1167
|
+
}
|
|
857
1168
|
|
|
858
1169
|
function selectAll() {
|
|
859
1170
|
if (!nodesList.length) return;
|
|
@@ -1049,11 +1360,14 @@
|
|
|
1049
1360
|
zonesFilterInitialized = true;
|
|
1050
1361
|
updateZonesFilterButtons();
|
|
1051
1362
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1363
|
+
renderZones(zones);
|
|
1364
|
+
if (!multiple) {
|
|
1365
|
+
handleStateBeeps(okStates[0].data && okStates[0].data.state);
|
|
1366
|
+
}
|
|
1367
|
+
if (showLog) {
|
|
1368
|
+
await loadLogs(okStates);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1057
1371
|
|
|
1058
1372
|
function startPolling() {
|
|
1059
1373
|
if (pollTimer) clearInterval(pollTimer);
|
|
@@ -1157,14 +1471,53 @@
|
|
|
1157
1471
|
if (els.btnZonesFilterOpen) {
|
|
1158
1472
|
els.btnZonesFilterOpen.addEventListener("click", () => setZonesFilter("open", { user: true }));
|
|
1159
1473
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1474
|
+
if (els.btnZonesFilterAll) {
|
|
1475
|
+
els.btnZonesFilterAll.addEventListener("click", () => setZonesFilter("all", { user: true }));
|
|
1476
|
+
}
|
|
1477
|
+
if (els.btnLogFilterAll) {
|
|
1478
|
+
els.btnLogFilterAll.addEventListener("click", () => setLogFilter("all"));
|
|
1479
|
+
}
|
|
1480
|
+
if (els.btnLogFilterAlarm) {
|
|
1481
|
+
els.btnLogFilterAlarm.addEventListener("click", () => setLogFilter("alarm"));
|
|
1482
|
+
}
|
|
1483
|
+
if (els.btnLogFilterArming) {
|
|
1484
|
+
els.btnLogFilterArming.addEventListener("click", () => setLogFilter("arming"));
|
|
1485
|
+
}
|
|
1486
|
+
if (els.btnLogFilterZones) {
|
|
1487
|
+
els.btnLogFilterZones.addEventListener("click", () => setLogFilter("zones"));
|
|
1488
|
+
}
|
|
1489
|
+
if (els.btnLogFilterErrors) {
|
|
1490
|
+
els.btnLogFilterErrors.addEventListener("click", () => setLogFilter("errors"));
|
|
1491
|
+
}
|
|
1492
|
+
if (els.btnLogDownload) {
|
|
1493
|
+
els.btnLogDownload.addEventListener("click", () => {
|
|
1494
|
+
const ids = Array.from(selectedIds);
|
|
1495
|
+
const out = [];
|
|
1496
|
+
ids.forEach((id) => {
|
|
1497
|
+
const alarmName = nodesList.find((n) => n && n.id === id)?.name || id;
|
|
1498
|
+
const list = logsById.get(id) || [];
|
|
1499
|
+
list.forEach((e) => out.push({ ...(e || {}), alarmId: id, alarmName }));
|
|
1500
|
+
});
|
|
1501
|
+
out.sort((a, b) => (Number(a.ts) || 0) - (Number(b.ts) || 0));
|
|
1502
|
+
const blob = new Blob([JSON.stringify(out, null, 2)], { type: "application/json" });
|
|
1503
|
+
const a = document.createElement("a");
|
|
1504
|
+
a.href = URL.createObjectURL(blob);
|
|
1505
|
+
a.download = `alarm-log-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
|
|
1506
|
+
document.body.appendChild(a);
|
|
1507
|
+
a.click();
|
|
1508
|
+
setTimeout(() => {
|
|
1509
|
+
try {
|
|
1510
|
+
URL.revokeObjectURL(a.href);
|
|
1511
|
+
a.remove();
|
|
1512
|
+
} catch (_err) {}
|
|
1513
|
+
}, 0);
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1163
1516
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1517
|
+
els.btnArm.addEventListener("click", async () => {
|
|
1518
|
+
playKeyClick("action");
|
|
1519
|
+
try {
|
|
1520
|
+
await sendCommand({ command: "arm", code: codeValue() });
|
|
1168
1521
|
if (els.cmdStatus.style.display === "none") {
|
|
1169
1522
|
showCmdStatus(`Arm sent to ${selectedIds.size} node(s).`, "ok");
|
|
1170
1523
|
}
|
|
@@ -1209,14 +1562,18 @@
|
|
|
1209
1562
|
else if (/^\d$/.test(e.key)) playKeyClick("key");
|
|
1210
1563
|
});
|
|
1211
1564
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1565
|
+
(async function init() {
|
|
1566
|
+
try {
|
|
1567
|
+
setNodeHint(`API root: ${httpAdminRoot()}`);
|
|
1568
|
+
updateLogFilterButtons();
|
|
1569
|
+
if (showLog && els.logHint) {
|
|
1570
|
+
els.logHint.textContent = "Loading...";
|
|
1571
|
+
}
|
|
1572
|
+
await loadNodes();
|
|
1573
|
+
await loadNodeStatesForButtons().catch(() => {});
|
|
1574
|
+
await loadState();
|
|
1575
|
+
startPolling();
|
|
1576
|
+
startNodeButtonPolling();
|
|
1220
1577
|
} catch (err) {
|
|
1221
1578
|
setNodeHint(err.message);
|
|
1222
1579
|
els.zonesHint.textContent = err.message;
|