Przejście na kanał zbiorczy LuPort + naprawa reconnectu, watchdog, widoczny offline
PRZYCZYNA ZNIKAJĄCYCH ALARMÓW ZNALEZIONA — i nie była nią liczba połączeń.
Commit 7cf0634 ("hidden status-bar & machine.status") zakomentował element
#status-bar w HTML, ale refreshStatus() nadal po niego sięgał. TypeError na
null przerywał handler ws.onclose PRZED zaplanowaniem reconnectu — każda
maszyna po pierwszym zerwaniu połączenia milkła NA ZAWSZE. Restart z crona
co 2 h maskował problem, zaczynając od zera.
Czerwcowe poprawki na kiosku (timery świeżości per-maszyna, ubijanie "zombie"
połączeń, jitter) szły w dobrą stronę, ale nieświadomie POGORSZYŁY objaw:
watchdog celowo zamykał ciche połączenie, a każde zamknięcie przechodziło
przez ten sam wadliwy onclose — własna obrona uśmiercała maszynę na stałe.
Ich sedno jest zachowane w tej wersji (wykrywanie ciszy, jitter, nakładkowy
iframe z "iframe optimalization").
NOWA ARCHITEKTURA (pod nowy system LuPort):
- JEDNO połączenie na kanał zbiorczy /ws/als-data/ zamiast 35 per-maszyna;
serwer wysyła migawkę stanu wszystkich maszyn zaraz po połączeniu (dodane
w LuPort równolegle), więc trwający alarm wraca na ekran natychmiast po
reconnexie
- token ApiUser w adresie (nowy system wymaga uwierzytelnienia WS); konto
bot-als-alert-screen bez żadnych uprawnień
- reconnect z wykładniczym odstępem (1->30 s) i jitterem; odstęp wraca do
minimum dopiero po 60 s stabilnego połączenia
- watchdog ciszy > 90 s (przeglądarka nie wystawia ping/pong do JS — martwe
półotwarte połączenie inaczej wisi w nieskończoność)
- dane maszyny starsze niż 5 min znikają z ekranu; przy rozłączeniu dane NIE
są czyszczone od razu (alarm nie znika przy 3-sekundowym czknięciu sieci —
o niepewności mówi pomarańczowy pasek, migawka po reconnexie przywraca
prawdę)
- utrata łączności WIDOCZNA: pomarańczowy pasek po 10 s + przywrócony pasek
statusu; element MUSI istnieć w DOM, refreshStatus() ma mimo to osłonę
- MACHINES stało się filtrem wyświetlania; ?ws= i ?token= nadpisują
konfigurację z adresu strony
- karty przez textContent; celowo BRAK ws.onerror (po błędzie przeglądarka
i tak wywołuje onclose)
TESTY (test/testy_ekranu.py): 10 deterministycznych scenariuszy bez sieci —
skrypt wycinany z żywego HTML, WebSocket podmieniony fałszywką, wirtualny
czas Chromium. Scenariusz 6 to bezpośredni test regresji błędu reconnectu;
mutacja (usunięcie planowania reconnectu) wywraca scenariusze 6, 8 i 9.
CRON RESTARTUJĄCY CO 2 H MOŻNA USUNĄĆ — README opisuje czemu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+231
-190
@@ -21,6 +21,10 @@
|
||||
}
|
||||
|
||||
/* ── NORMAL VIEW ──────────────────────────────── */
|
||||
/* Iframe jest ZAWSZE zamontowany, a widok alarmowy to nakładka nad nim
|
||||
(pomysł z wersji kioskowej, "iframe optimalization"). Przełączanie
|
||||
przez display:none wymuszało przeładowanie treści iframe'a przy każdym
|
||||
powrocie z alarmu. */
|
||||
#normal-view {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -38,20 +42,17 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 2; /* Warstwa na wierzchu */
|
||||
|
||||
/* Zamiast display: none, używamy przezroczystości i wyłączamy kliknięcia */
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0d0000;
|
||||
transition: opacity 0.3s ease; /* Łagodne przejście */
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#alarm-view.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto; /* Włączenie interakcji, jeśli potrzebne */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.alarm-header {
|
||||
@@ -144,64 +145,42 @@
|
||||
text-shadow: 0 0 24px rgba(255, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.card-status {
|
||||
font-size: min(
|
||||
calc(7vw / var(--cols, 1)),
|
||||
calc(7vh / var(--rows, 1)),
|
||||
5cqw
|
||||
);
|
||||
font-weight: 700;
|
||||
color: #ff9999;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
/* ── OFFLINE BANNER ──────────────────────────── */
|
||||
/* Widoczna z daleka informacja o utracie łączności z LuPort.
|
||||
POMARAŃCZOWA, nie czerwona — czerwień jest zarezerwowana dla alarmów
|
||||
maszyn; ten pasek mówi „ekran może pokazywać nieświeży obraz",
|
||||
a nie „maszyna stoi". Poprzednia wersja tej strony traciła połączenia
|
||||
PO CICHU (patrz README, sekcja Historia) — ten pasek jest wprost
|
||||
wnioskiem z tamtej awarii: utrata łączności ma być widoczna z hali,
|
||||
a nie odkrywana po fakcie. */
|
||||
#offline-banner {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9998;
|
||||
background: #7a4a00;
|
||||
color: #ffd9a0;
|
||||
border-bottom: 3px solid #ffaa33;
|
||||
padding: 10px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── detail grid ── */
|
||||
.card-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 14px;
|
||||
border-top: 1px solid #3a0000;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.info-label {
|
||||
font-size: clamp(9px, 0.8vw, 12px);
|
||||
color: #664444;
|
||||
text-transform: uppercase;
|
||||
font-size: clamp(16px, 2vw, 28px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.info-value {
|
||||
font-size: clamp(11px, 1.1vw, 16px);
|
||||
color: #ddcccc;
|
||||
font-weight: 700;
|
||||
#offline-banner.active {
|
||||
display: block;
|
||||
animation: offlineBlink 2s step-start infinite;
|
||||
}
|
||||
|
||||
.progress-row {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 12px;
|
||||
background: #3a0000;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #990000, #ff4444);
|
||||
border-radius: 6px;
|
||||
transition: width 0.8s ease;
|
||||
}
|
||||
.progress-label {
|
||||
text-align: right;
|
||||
font-size: clamp(9px, 0.8vw, 11px);
|
||||
color: #775555;
|
||||
margin-top: 3px;
|
||||
@keyframes offlineBlink {
|
||||
0%,
|
||||
100% {
|
||||
background: #7a4a00;
|
||||
}
|
||||
50% {
|
||||
background: #9a5e00;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── STATUS BAR ──────────────────────────────── */
|
||||
@@ -236,167 +215,217 @@
|
||||
<div id="machines-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- <div id="status-bar">WS: 0 / 0</div> -->
|
||||
<div id="offline-banner">⚠ BRAK POŁĄCZENIA Z LUPORT — DANE MOGĄ BYĆ NIEAKTUALNE ⚠</div>
|
||||
|
||||
<!-- Ten element MUSI istnieć w DOM. Poprzednia wersja miała go
|
||||
zakomentowanego, a refreshStatus() dalej po niego sięgał — TypeError
|
||||
w ws.onclose przerywał handler PRZED zaplanowaniem reconnectu i każda
|
||||
maszyna po pierwszym zerwaniu połączenia milkła na zawsze (naprawiane
|
||||
restartem z crona co 2 h). Jeśli pasek ma być niewidoczny, ukryj go
|
||||
przez CSS (display:none), nigdy przez usunięcie/komentarz. -->
|
||||
<div id="status-bar">WS: łączenie…</div>
|
||||
|
||||
<script>
|
||||
// ══ KONFIGURACJA ══════════════════════════════════════════════════════
|
||||
|
||||
// Kanał ZBIORCZY nowego systemu — jedno połączenie niesie aktualizacje
|
||||
// wszystkich maszyn (stara wersja: osobne połączenie na każdą z 35
|
||||
// maszyn). Po nawiązaniu połączenia serwer wysyła migawkę stanu
|
||||
// wszystkich maszyn (ramki "machine_data"), więc po reconnexie trwający
|
||||
// alarm wraca na ekran natychmiast, nie po cyklu scrapera.
|
||||
const WS_URL = "ws://luport.local/ws/als-data/";
|
||||
|
||||
// Nowy system wymaga uwierzytelnienia WebSocket. Wstaw token konta
|
||||
// ApiUser (Django Admin -> Users -> Api users -> "bot-als-alert-screen";
|
||||
// konto NIE potrzebuje żadnych uprawnień — sam odbiór strumienia ich nie
|
||||
// wymaga). Można też podać token w adresie strony: ?token=...
|
||||
const WS_TOKEN = "WSTAW-TOKEN-APIUSER";
|
||||
|
||||
// Filtr maszyn pokazywanych na ekranie. Kanał zbiorczy niesie WSZYSTKIE
|
||||
// maszyny znane LuPortowi — pusta lista ([]) oznacza „pokazuj wszystkie".
|
||||
const MACHINES = [
|
||||
"1.64",
|
||||
"1.76",
|
||||
"2.60",
|
||||
"2.63",
|
||||
"2.67",
|
||||
"2.72",
|
||||
"2.74",
|
||||
"3.01",
|
||||
"3.04",
|
||||
"3.08",
|
||||
"3.22",
|
||||
"3.26",
|
||||
"3.51",
|
||||
"3.55",
|
||||
"3.56",
|
||||
"3.59",
|
||||
"3.61",
|
||||
"3.66",
|
||||
"3.69",
|
||||
"3.70",
|
||||
"3.71",
|
||||
"3.73",
|
||||
"3.75",
|
||||
"4.05",
|
||||
"4.09",
|
||||
"4.14",
|
||||
"4.52",
|
||||
"4.53",
|
||||
"4.57",
|
||||
"4.58",
|
||||
"4.62",
|
||||
"4.65",
|
||||
"4.68",
|
||||
"5.29",
|
||||
"5.54",
|
||||
"1.64", "1.76", "2.60", "2.63", "2.67", "2.72", "2.74",
|
||||
"3.01", "3.04", "3.08", "3.22", "3.26", "3.51", "3.55",
|
||||
"3.56", "3.59", "3.61", "3.66", "3.69", "3.70", "3.71",
|
||||
"3.73", "3.75", "4.05", "4.09", "4.14", "4.52", "4.53",
|
||||
"4.57", "4.58", "4.62", "4.65", "4.68", "5.29", "5.54",
|
||||
];
|
||||
|
||||
const WS_BASE = "ws://192.168.40.127/ws/als-data/";
|
||||
const RECONNECT_MS = 5000;
|
||||
// Reconnect: wykładniczy odstęp z losowym rozrzutem, żeby po awarii
|
||||
// serwera ekran nie młócił połączeń co sekundę w nieskończoność.
|
||||
const RECONNECT_MIN_MS = 1000;
|
||||
const RECONNECT_MAX_MS = 30000;
|
||||
const RECONNECT_JITTER_MS = 2000;
|
||||
|
||||
// ── Debug mode ────────────────────────────────────────────────────────────
|
||||
// Otwórz stronę z ?debug aby wymusić widok alarmowy z przykładowymi danymi.
|
||||
// Możesz też wpisać w konsoli: DEBUG_ALARM = true; updateDisplay();
|
||||
// Watchdog martwego połączenia. Przeglądarka NIE wystawia ping/pong do
|
||||
// JS — gdy trasa sieciowa umrze bez zamknięcia TCP, `onclose` może nie
|
||||
// przyjść nigdy (dokładnie ten scenariusz wcześniej „leczył" restart
|
||||
// z crona). Na kanale zbiorczym dane płyną praktycznie ciągle (35 maszyn
|
||||
// x cykl scrapera 30 s), więc cisza dłuższa niż WATCHDOG_SILENCE_MS
|
||||
// oznacza martwe połączenie: zamykamy je ręcznie, co uruchamia zwykłą
|
||||
// ścieżkę reconnectu.
|
||||
const WATCHDOG_SILENCE_MS = 90000;
|
||||
const WATCHDOG_CHECK_MS = 15000;
|
||||
|
||||
// Dane maszyny starsze niż STALE_MS znikają z ekranu. Bez tego alarm
|
||||
// maszyny usuniętej z widgetu ALS (albo trwale wyłączonej) wisiałby na
|
||||
// ekranie w nieskończoność — klucz w Redisie wygasa po 2x cyklu
|
||||
// scrapera, więc świeże dane po prostu przestają przychodzić.
|
||||
const STALE_MS = 5 * 60 * 1000;
|
||||
|
||||
// Po jakim czasie bez połączenia pokazać pomarańczowy pasek. Krótkie
|
||||
// przerwy (restart api przy aktualizacji) nie muszą straszyć hali.
|
||||
const OFFLINE_BANNER_AFTER_MS = 10000;
|
||||
|
||||
// ══ Debug mode ════════════════════════════════════════════════════════
|
||||
// Otwórz stronę z ?debug aby wymusić widok alarmowy z przykładowymi
|
||||
// danymi (bez połączenia WS). W konsoli: DEBUG_ALARM = true; updateDisplay();
|
||||
let DEBUG_ALARM = new URLSearchParams(location.search).has("debug");
|
||||
|
||||
const DEBUG_MACHINES = ["2.60", "3.22", "4.09"];
|
||||
const DEBUG_DATA = {
|
||||
"2.60": { machine: "2.60", status: "Alarm - brak materiału" },
|
||||
3.22: { machine: "3.22", status: "Alarm - awaria" },
|
||||
4.09: { machine: "4.09", status: "Alarm - przekroczony cykl" },
|
||||
"3.22": { machine: "3.22", status: "Alarm - awaria" },
|
||||
"4.09": { machine: "4.09", status: "Alarm - przekroczony cykl" },
|
||||
};
|
||||
|
||||
const machineData = {}; // id → last data payload
|
||||
const wsStatus = {}; // id → 'connected' | 'error' | 'disconnected'
|
||||
const wsMap = {}; // id → WebSocket instance
|
||||
const dataTimers = {}; // id → stale-data timeout handle
|
||||
// ══ Stan ══════════════════════════════════════════════════════════════
|
||||
|
||||
// Jeśli przez DATA_STALE_MS nie przyjdzie żadna wiadomość od maszyny
|
||||
// (nawet przy technicznie otwartym WS), dane uznajemy za nieaktualne.
|
||||
const DATA_STALE_MS = 2 * 60 * 1000; // 2 minuty
|
||||
const machineData = {}; // id -> { data, receivedAt }
|
||||
let ws = null;
|
||||
let wsConnected = false;
|
||||
let reconnectDelay = RECONNECT_MIN_MS;
|
||||
let reconnectTimer = null;
|
||||
let stableTimer = null;
|
||||
let lastMessageAt = 0;
|
||||
let disconnectedSince = Date.now();
|
||||
|
||||
const iframe = document.getElementById("normal-view");
|
||||
const machineFilter = MACHINES.length ? new Set(MACHINES) : null;
|
||||
|
||||
iframe.onload = () => {
|
||||
console.log("Iframe załadowany, startuję WebSockety...");
|
||||
// Dajemy stronie w iframe jeszcze sekundę na przetworzenie własnego JS
|
||||
setTimeout(() => {
|
||||
MACHINES.forEach(connect);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (Object.keys(wsMap).length === 0) {
|
||||
MACHINES.forEach(connect);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
function resetStaleTimer(id) {
|
||||
clearTimeout(dataTimers[id]);
|
||||
dataTimers[id] = setTimeout(() => {
|
||||
delete machineData[id];
|
||||
updateDisplay();
|
||||
|
||||
// NOWE: Jeśli dane są przestarzałe, zakładamy, że WebSocket to "zombie".
|
||||
// Zabijamy go ręcznie, co wywoła onclose i sprowokuje ponowne połączenie.
|
||||
if (wsMap[id] && wsStatus[id] === "connected") {
|
||||
console.warn(`Brak danych dla ${id} - ubijam ciche połączenie`);
|
||||
wsMap[id].close();
|
||||
}
|
||||
}, DATA_STALE_MS);
|
||||
function wsUrlWithToken() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
// Oba parametry mają nadpisania w adresie strony (?ws=...&token=...) —
|
||||
// kiosk można wycelować w inny serwer bez edycji pliku, a testy strony
|
||||
// w serwer lokalny.
|
||||
const base = params.get("ws") || WS_URL;
|
||||
const token = params.get("token") || WS_TOKEN;
|
||||
return base + "?token=" + encodeURIComponent(token);
|
||||
}
|
||||
|
||||
// ── WebSocket management ──────────────────────────────────────────────────
|
||||
// ══ Połączenie ════════════════════════════════════════════════════════
|
||||
|
||||
function connect(id) {
|
||||
if (wsMap[id]) {
|
||||
function connect() {
|
||||
clearTimeout(reconnectTimer);
|
||||
if (ws) {
|
||||
try {
|
||||
wsMap[id].close();
|
||||
ws.onclose = null; // stare gniazdo nie może zaplanować DRUGIEGO reconnectu
|
||||
ws.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const ws = new WebSocket(WS_BASE + id + "/");
|
||||
wsMap[id] = ws;
|
||||
wsStatus[id] = "connecting";
|
||||
ws = new WebSocket(wsUrlWithToken());
|
||||
|
||||
ws.onopen = () => {
|
||||
wsStatus[id] = "connected";
|
||||
wsConnected = true;
|
||||
lastMessageAt = Date.now();
|
||||
// Odstęp reconnectu wraca do minimum dopiero po chwili STABILNEGO
|
||||
// połączenia — reset w onopen pozwalałby serwerowi, który przyjmuje
|
||||
// i zaraz zrywa (np. zły token -> close 4001), młócić co sekundę.
|
||||
clearTimeout(stableTimer);
|
||||
stableTimer = setTimeout(() => {
|
||||
reconnectDelay = RECONNECT_MIN_MS;
|
||||
}, 60000);
|
||||
refreshStatus();
|
||||
resetStaleTimer(id); // NOWE: Startujemy timer od razu po otwarciu
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
lastMessageAt = Date.now();
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.data) {
|
||||
machineData[id] = { ...msg.data, _ts: Date.now() };
|
||||
resetStaleTimer(id);
|
||||
// "machine_data" = migawka po połączeniu, "machine_update" = strumień
|
||||
// na żywo. Ta sama obsługa: liczy się najnowszy znany stan maszyny.
|
||||
if (
|
||||
(msg.type === "machine_data" || msg.type === "machine_update") &&
|
||||
msg.data &&
|
||||
msg.machine
|
||||
) {
|
||||
if (machineFilter && !machineFilter.has(msg.machine)) return;
|
||||
machineData[msg.machine] = {
|
||||
data: { ...msg.data, machine: msg.machine },
|
||||
receivedAt: Date.now(),
|
||||
};
|
||||
updateDisplay();
|
||||
refreshStatus();
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore malformed frames */
|
||||
/* ignoruj uszkodzone ramki */
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
wsStatus[id] = "error";
|
||||
refreshStatus();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
wsStatus[id] = "disconnected";
|
||||
clearTimeout(dataTimers[id]);
|
||||
delete machineData[id];
|
||||
updateDisplay();
|
||||
wsConnected = false;
|
||||
disconnectedSince = Date.now();
|
||||
clearTimeout(stableTimer);
|
||||
refreshStatus();
|
||||
|
||||
const reconnectDelay =
|
||||
RECONNECT_MS + Math.floor(Math.random() * 3000);
|
||||
setTimeout(() => connect(id), reconnectDelay);
|
||||
reconnectTimer = setTimeout(connect, nextReconnectDelay());
|
||||
};
|
||||
|
||||
// Celowo brak ws.onerror: po błędzie przeglądarka i tak wywołuje
|
||||
// onclose, a osobny handler był w starej wersji tylko drugim miejscem,
|
||||
// z którego dało się rzucić wyjątkiem.
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
// ── Display logic ─────────────────────────────────────────────────────────
|
||||
function nextReconnectDelay() {
|
||||
const delay = reconnectDelay + Math.random() * RECONNECT_JITTER_MS;
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
||||
return delay;
|
||||
}
|
||||
|
||||
function isAlarm(data) {
|
||||
if (DEBUG_ALARM) return true; // w trybie debug wszystkie wstrzyknięte maszyny to alarmy
|
||||
// Watchdog + odświeżenie widoku (przeterminowane dane, pasek offline)
|
||||
// w jednym cyklu.
|
||||
setInterval(() => {
|
||||
if (DEBUG_ALARM) return;
|
||||
if (
|
||||
wsConnected &&
|
||||
ws &&
|
||||
ws.readyState === WebSocket.OPEN &&
|
||||
Date.now() - lastMessageAt > WATCHDOG_SILENCE_MS
|
||||
) {
|
||||
// close() uruchomi onclose -> zwykła ścieżka reconnectu.
|
||||
try {
|
||||
ws.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
updateDisplay();
|
||||
refreshStatus();
|
||||
}, WATCHDOG_CHECK_MS);
|
||||
|
||||
// ══ Widok ═════════════════════════════════════════════════════════════
|
||||
|
||||
function isAlarm(entry) {
|
||||
return (
|
||||
data &&
|
||||
typeof data.status === "string" &&
|
||||
data.status.toLowerCase().includes("alarm")
|
||||
entry &&
|
||||
entry.data &&
|
||||
typeof entry.data.status === "string" &&
|
||||
entry.data.status.toLowerCase().includes("alarm")
|
||||
);
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
const alarms = Object.entries(machineData)
|
||||
.filter(([, d]) => isAlarm(d))
|
||||
function isFresh(entry) {
|
||||
return entry && Date.now() - entry.receivedAt < STALE_MS;
|
||||
}
|
||||
|
||||
function currentAlarms() {
|
||||
if (DEBUG_ALARM) {
|
||||
return Object.entries(DEBUG_DATA).map(([id, d]) => [id, { data: d }]);
|
||||
}
|
||||
return Object.entries(machineData)
|
||||
.filter(([, entry]) => isFresh(entry) && isAlarm(entry))
|
||||
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
const alarms = currentAlarms();
|
||||
|
||||
const alarmView = document.getElementById("alarm-view");
|
||||
const countEl = document.getElementById("alarm-count");
|
||||
@@ -423,32 +452,44 @@
|
||||
grid.style.setProperty("--cols", cols);
|
||||
grid.style.setProperty("--rows", rows);
|
||||
|
||||
alarms.forEach(([id, d]) => {
|
||||
alarms.forEach(([id, entry]) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "machine-card";
|
||||
card.innerHTML = `
|
||||
<div class="card-machine-id">${d.machine ?? id}</div>
|
||||
<!-- <div class="card-status">${d.status ?? "ALARM"}</div> -->
|
||||
`;
|
||||
const machineId = document.createElement("div");
|
||||
machineId.className = "card-machine-id";
|
||||
machineId.textContent = entry.data.machine ?? id;
|
||||
card.appendChild(machineId);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
const connected = Object.values(wsStatus).filter(
|
||||
(s) => s === "connected",
|
||||
).length;
|
||||
document.getElementById("status-bar").textContent =
|
||||
`WS: ${connected} / ${MACHINES.length}`;
|
||||
// Osłona zamiast założenia, że element istnieje — dokładnie ten brak
|
||||
// osłony ubijał reconnect w poprzedniej wersji (patrz komentarz przy
|
||||
// #status-bar w HTML).
|
||||
const bar = document.getElementById("status-bar");
|
||||
if (bar) {
|
||||
const fresh = Object.values(machineData).filter(isFresh).length;
|
||||
bar.textContent = wsConnected
|
||||
? `WS: połączono · maszyny z danymi: ${fresh}`
|
||||
: "WS: rozłączono — ponawiam…";
|
||||
}
|
||||
|
||||
const banner = document.getElementById("offline-banner");
|
||||
if (banner) {
|
||||
const offlineLongEnough =
|
||||
!wsConnected && Date.now() - disconnectedSince > OFFLINE_BANNER_AFTER_MS;
|
||||
banner.classList.toggle("active", offlineLongEnough && !DEBUG_ALARM);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
||||
// ══ Start ═════════════════════════════════════════════════════════════
|
||||
|
||||
if (DEBUG_ALARM) {
|
||||
DEBUG_MACHINES.forEach((id) => {
|
||||
machineData[id] = DEBUG_DATA[id];
|
||||
});
|
||||
document.getElementById("status-bar").textContent =
|
||||
"[DEBUG] widok alarmowy";
|
||||
const bar = document.getElementById("status-bar");
|
||||
if (bar) bar.textContent = "[DEBUG] widok alarmowy";
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
updateDisplay();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user