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:
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
@@ -1,145 +1,160 @@
|
||||
# ALS Alert Screen
|
||||
|
||||
Jednostronicowa aplikacja HTML/JS wyświetlająca alarmy maszyn na podstawie danych z WebSocket. Przeznaczona do montażu na monitorze halowym.
|
||||
Jednostronicowa aplikacja HTML/JS wyświetlająca alarmy maszyn na podstawie
|
||||
danych WebSocket z LuPort. Przeznaczona do montażu na monitorze halowym
|
||||
(Chromium na Debianie).
|
||||
|
||||
---
|
||||
|
||||
## Działanie
|
||||
|
||||
Strona nasłuchuje danych z WebSocket dla każdej skonfigurowanej maszyny. W zależności od statusów maszyn wyświetla jeden z dwóch widoków:
|
||||
Strona utrzymuje **jedno** połączenie WebSocket ze zbiorczym kanałem ALS
|
||||
nowego systemu (`/ws/als-data/` bez maszyny w adresie) — kanał niesie
|
||||
aktualizacje wszystkich maszyn, a zaraz po połączeniu serwer wysyła migawkę
|
||||
ich bieżącego stanu, więc trwający alarm pojawia się na ekranie natychmiast,
|
||||
także po każdym wznowieniu połączenia.
|
||||
|
||||
| Sytuacja | Widok |
|
||||
|----------|-------|
|
||||
| Brak maszyny w alarmie | Iframe z `http://diluals31/terminal/ift/5/PL/index` |
|
||||
| Brak maszyny w alarmie | Iframe z infoterminalem ALS |
|
||||
| Co najmniej jedna maszyna w alarmie | Ekran alarmowy z kartami maszyn |
|
||||
| Brak łączności z LuPort > 10 s | Pomarańczowy pasek u góry (na obu widokach) |
|
||||
|
||||
Alarm jest wykrywany gdy pole `status` w danych maszyny zawiera słowo **`alarm`** (bez rozróżnienia wielkości liter), np. `"Alarm - awaria"`, `"ALARM"`.
|
||||
Alarm jest wykrywany, gdy pole `status` maszyny zawiera słowo **`alarm`**
|
||||
(bez rozróżnienia wielkości liter), np. `"Alarm - awaria"`, `"ALARM"`.
|
||||
|
||||
---
|
||||
|
||||
## Ekran alarmowy
|
||||
|
||||
- Karty maszyn wypełniają cały dostępny ekran w układzie **siatki N×M** (możliwie kwadratowej).
|
||||
- Numer maszyny jest wyświetlany bardzo dużą czcionką, widoczną z daleka — rozmiar skaluje się automatycznie zarówno do wymiarów ekranu (`vw`/`vh`), jak i szerokości karty (`cqw`), więc tekst nigdy nie jest ucięty.
|
||||
- Pod numerem wyświetlany jest pełny tekst statusu.
|
||||
- Pasek u góry pokazuje liczbę maszyn w alarmie i miga.
|
||||
|
||||
### Układ siatki w zależności od liczby alarmów
|
||||
|
||||
| Alarmy | Kolumny | Wiersze |
|
||||
|--------|---------|---------|
|
||||
| 1 | 1 | 1 |
|
||||
| 2 | 2 | 1 |
|
||||
| 3 | 2 | 2 |
|
||||
| 4 | 2 | 2 |
|
||||
| 5 | 3 | 2 |
|
||||
| 6 | 3 | 2 |
|
||||
| 9 | 3 | 3 |
|
||||
Pomarańczowy pasek to celowo INNY kolor niż czerwień alarmów: mówi „ekran może
|
||||
pokazywać nieświeży obraz", a nie „maszyna stoi".
|
||||
|
||||
---
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
Wszystkie parametry konfiguracyjne znajdują się na początku sekcji `<script>` w pliku `als_alert_screen.html`.
|
||||
Wszystkie parametry na początku sekcji `<script>` w `als_alert_screen.html`:
|
||||
|
||||
### Lista maszyn
|
||||
| Stała | Znaczenie |
|
||||
|---|---|
|
||||
| `WS_URL` | adres kanału zbiorczego, np. `ws://luport.local/ws/als-data/` |
|
||||
| `WS_TOKEN` | token konta `ApiUser` (patrz niżej) |
|
||||
| `MACHINES` | filtr maszyn pokazywanych na ekranie; `[]` = wszystkie |
|
||||
| `WATCHDOG_SILENCE_MS` | po jakiej ciszy uznać połączenie za martwe (90 s) |
|
||||
| `STALE_MS` | po jakim czasie dane maszyny znikają z ekranu (5 min) |
|
||||
| `OFFLINE_BANNER_AFTER_MS` | po jakim czasie bez łączności pokazać pasek (10 s) |
|
||||
|
||||
```js
|
||||
const MACHINES = [
|
||||
"1.64", "1.76", "2.60", ...
|
||||
];
|
||||
Oba kluczowe parametry można też nadpisać w adresie strony — wygodne przy
|
||||
konfiguracji kiosku bez edycji pliku:
|
||||
|
||||
```
|
||||
als_alert_screen.html?ws=ws%3A%2F%2Fluport.local%2Fws%2Fals-data%2F&token=...
|
||||
```
|
||||
|
||||
Każdy wpis to numer maszyny używany w adresie WebSocket. Aby dodać lub usunąć maszynę — edytuj tę tablicę.
|
||||
### Token (wymagany przez nowy system)
|
||||
|
||||
### Adres WebSocket
|
||||
Nowy LuPort wymaga uwierzytelnienia WebSocket. Utwórz dedykowane konto:
|
||||
Django Admin → **Users → Api users** → dodaj `bot-als-alert-screen`
|
||||
**bez żadnych uprawnień** (sam odbiór strumienia ALS ich nie wymaga — konto
|
||||
ma minimalny możliwy dostęp). Wygenerowany token wpisz w `WS_TOKEN`.
|
||||
|
||||
```js
|
||||
const WS_BASE = "ws://luport.local/ws/als-data/";
|
||||
```
|
||||
### Format wiadomości
|
||||
|
||||
Dla każdej maszyny otwierane jest połączenie pod adresem `WS_BASE + id + "/"`,
|
||||
np. `ws://luport.local/ws/als-data/2.60/`.
|
||||
Strona obsługuje dwa typy ramek — obydwa tak samo (liczy się najnowszy stan):
|
||||
|
||||
### Format wiadomości WebSocket
|
||||
- `machine_data` — migawka wysyłana przez serwer zaraz po połączeniu,
|
||||
- `machine_update` — aktualizacje na żywo.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "machine_update",
|
||||
"machine": "2.60",
|
||||
"data": {
|
||||
"machine": "2.60",
|
||||
"status": "Automatyczny",
|
||||
"mold": 6447,
|
||||
"variant": [3],
|
||||
"cavities": 2,
|
||||
"order": "202605350 0010",
|
||||
"qty_planned": 8400,
|
||||
"qty_produced_ok": 2132,
|
||||
"qty_produced_nok": 10,
|
||||
"cycle_time_planned": 58.0,
|
||||
"cycle_time_average": 57.4,
|
||||
"progress": 25
|
||||
},
|
||||
"timestamp": "2026-05-28T12:00:32.898808"
|
||||
"data": { "status": "Automatyczny", "orders": ["202605350"], "progress": 25, "...": "..." },
|
||||
"timestamp": "2026-07-31T12:00:32"
|
||||
}
|
||||
```
|
||||
|
||||
Strona obsługuje tylko wiadomości z `"type": "machine_update"`. Pozostałe są ignorowane.
|
||||
---
|
||||
|
||||
### Czas ponownego połączenia
|
||||
## Odporność na awarie — dlaczego cron-restart nie jest już potrzebny
|
||||
|
||||
```js
|
||||
const RECONNECT_MS = 5000;
|
||||
1. **Reconnect z wykładniczym odstępem i jitterem** (1 s → 30 s). Odstęp wraca
|
||||
do minimum dopiero po 60 s stabilnego połączenia — serwer, który przyjmuje
|
||||
i zaraz zrywa (np. zły token), nie jest młócony co sekundę.
|
||||
2. **Watchdog martwego połączenia.** Przeglądarka nie wystawia ping/pong do
|
||||
JS, więc połączenie zabite bez zamknięcia TCP potrafi wisieć „otwarte"
|
||||
w nieskończoność. Na kanale zbiorczym dane płyną praktycznie ciągle
|
||||
(35 maszyn × cykl scrapera 30 s) — cisza dłuższa niż 90 s oznacza martwe
|
||||
połączenie i wymusza reconnect.
|
||||
3. **Wygasanie danych.** Dane maszyny starsze niż 5 min znikają z ekranu —
|
||||
alarm maszyny usuniętej z widgetu ALS nie wisi w nieskończoność.
|
||||
4. **Widoczna utrata łączności.** Pasek offline + pasek statusu w rogu.
|
||||
Ekran nigdy nie pokazuje po cichu nieświeżego obrazu.
|
||||
|
||||
---
|
||||
|
||||
## Historia: dlaczego alarmy „przestawały wyskakiwać" (do 2026-07-31)
|
||||
|
||||
Poprzednia wersja utrzymywała **35 osobnych połączeń** (po jednym na maszynę)
|
||||
i miała błąd, który ujawnił się po ukryciu paska statusu: element `#status-bar`
|
||||
został **zakomentowany 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.
|
||||
Maszyny odpadały jedna po drugiej przy każdym czknięciu sieci, a restart
|
||||
z crona co 2 h maskował problem, zaczynając od zera.
|
||||
|
||||
Wnioski utrwalone w obecnym kodzie:
|
||||
|
||||
- element statusu **musi istnieć w DOM** (ukrywanie wyłącznie przez CSS),
|
||||
a `refreshStatus()` i tak ma osłonę na jego brak,
|
||||
- brak `ws.onerror` jest celowy — po błędzie przeglądarka i tak wywołuje
|
||||
`onclose`, a osobny handler był tylko drugim miejscem, z którego dało się
|
||||
rzucić wyjątkiem,
|
||||
- scenariusz 6 w testach (`test/testy_ekranu.py`) jest bezpośrednim testem
|
||||
regresji tego błędu.
|
||||
|
||||
Poprawki robione na kiosku w czerwcu (timery świeżości per-maszyna,
|
||||
ubijanie „zombie" połączeń, jitter reconnectu) 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` — czyli
|
||||
własna obrona uśmiercała maszynę na stałe. Ich sedno (wykrywanie ciszy,
|
||||
jitter) jest zachowane w obecnej wersji; nakładkowy iframe („iframe
|
||||
optimalization") również. Zrezygnowano z czyszczenia danych przy rozłączeniu:
|
||||
alarm nie znika przy 3-sekundowym czknięciu sieci — o niepewności informuje
|
||||
pomarańczowy pasek, a migawka po reconnexie przywraca prawdę.
|
||||
|
||||
**Cron restartujący przeglądarkę co 2 h można usunąć.**
|
||||
|
||||
---
|
||||
|
||||
## Testy
|
||||
|
||||
```bash
|
||||
python3 test/testy_ekranu.py
|
||||
# lub na kiosku:
|
||||
CHROME_BIN=/usr/bin/chromium python3 test/testy_ekranu.py
|
||||
```
|
||||
|
||||
Przy zerwaniu połączenia WS strona automatycznie próbuje ponownie po 5 sekundach.
|
||||
Deterministyczne, bez sieci: skrypt aplikacji jest wycinany z żywego
|
||||
`als_alert_screen.html`, `window.WebSocket` podmieniany na fałszywkę,
|
||||
a scenariusz (10 asercji: migawka, aktualizacje, reconnect, baner offline,
|
||||
watchdog, wygasanie danych) jedzie na wirtualnym czasie Chromium — całość
|
||||
trwa kilka sekund mimo symulowania ~6 minut.
|
||||
|
||||
---
|
||||
|
||||
## Tryb debug
|
||||
|
||||
Służy do podglądu ekranu alarmowego bez rzeczywistych danych WS.
|
||||
|
||||
### Przez URL
|
||||
|
||||
Otwórz stronę z parametrem `?debug`:
|
||||
Podgląd ekranu alarmowego bez danych i bez połączeń WS:
|
||||
|
||||
```
|
||||
als_alert_screen.html?debug
|
||||
```
|
||||
|
||||
Ładuje przykładowe dane dla maszyn `2.60`, `3.22`, `4.09` i natychmiast pokazuje ekran alarmowy. Połączenia WS **nie są** nawiązywane. Pasek statusu wyświetla `[DEBUG] widok alarmowy`.
|
||||
|
||||
### Przez konsolę przeglądarki
|
||||
|
||||
W każdej chwili można przełączyć widok bez przeładowania strony:
|
||||
|
||||
```js
|
||||
// włącz widok alarmowy
|
||||
DEBUG_ALARM = true; updateDisplay();
|
||||
|
||||
// wróć do normalnego widoku
|
||||
DEBUG_ALARM = false; updateDisplay();
|
||||
```
|
||||
|
||||
### Dane przykładowe
|
||||
|
||||
Maszyny i dane używane w trybie debug są zdefiniowane w zmiennych `DEBUG_MACHINES` i `DEBUG_DATA` — można je swobodnie edytować.
|
||||
|
||||
---
|
||||
|
||||
## Pasek statusu
|
||||
|
||||
W prawym dolnym rogu ekranu widoczny jest mały pasek z liczbą aktywnych połączeń WebSocket:
|
||||
|
||||
```
|
||||
WS: 32 / 35
|
||||
```
|
||||
albo w konsoli przeglądarki: `DEBUG_ALARM = true; updateDisplay();`
|
||||
(powrót: `DEBUG_ALARM = false; updateDisplay();`).
|
||||
|
||||
---
|
||||
|
||||
## Wymagania
|
||||
|
||||
- Przeglądarka z obsługą WebSocket, CSS Container Queries (`cqw`) i `min()` w CSS — spełnia każda nowoczesna przeglądarka (Chrome 105+, Firefox 110+, Edge 105+).
|
||||
- Dostęp sieciowy do `luport.local` (WebSocket) oraz `diluals31` (iframe).
|
||||
- Chromium/Chrome 105+ (CSS Container Queries, `min()`) — kiosk działa na
|
||||
Chromium pod Debianem.
|
||||
- Dostęp sieciowy do LuPort (WebSocket) oraz infoterminala ALS (iframe).
|
||||
|
||||
+229
-188
@@ -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;
|
||||
@keyframes offlineBlink {
|
||||
0%,
|
||||
100% {
|
||||
background: #7a4a00;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 12px;
|
||||
background: #3a0000;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
50% {
|
||||
background: #9a5e00;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ── 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…";
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ══ 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>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Testy ekranu alarmowego — deterministyczne, bez sieci i bez serwera.
|
||||
|
||||
Skrypt aplikacji jest wycinany z ŻYWEGO als_alert_screen.html (nie z kopii,
|
||||
więc testy nie mogą się rozjechać z kodem), a `window.WebSocket` podmieniany
|
||||
na fałszywkę sterowaną scenariuszem. Cały przebieg — łącznie z 90-sekundowym
|
||||
watchdogiem i 5-minutowym wygasaniem danych — jedzie na wirtualnym czasie
|
||||
Chromium (`--virtual-time-budget`), więc trwa sekundy.
|
||||
|
||||
Uruchomienie:
|
||||
python3 test/testy_ekranu.py
|
||||
CHROME_BIN=/usr/bin/chromium python3 test/testy_ekranu.py
|
||||
|
||||
Kod wyjścia 0 = wszystkie scenariusze PASS.
|
||||
|
||||
Dlaczego to istnieje: poprzednia wersja strony miała błąd, przez który KAŻDA
|
||||
maszyna po pierwszym zerwaniu połączenia milkła na zawsze (wyjątek w onclose
|
||||
przerywał handler przed zaplanowaniem reconnectu — patrz README, Historia).
|
||||
Objaw „alarmy przestały wyskakiwać" był leczony restartem z crona co 2 h.
|
||||
Scenariusz 6 jest bezpośrednim testem regresji tego błędu.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
APP = Path(__file__).resolve().parent.parent / "als_alert_screen.html"
|
||||
|
||||
SCENARIUSZ = """
|
||||
const wyniki = [];
|
||||
function assert(nazwa, warunek) {
|
||||
wyniki.push((warunek ? "PASS " : "FAIL ") + nazwa);
|
||||
document.getElementById("wynik").textContent = wyniki.join("\\n");
|
||||
}
|
||||
const alarmActive = () => document.getElementById("alarm-view").classList.contains("active");
|
||||
const cards = () => [...document.querySelectorAll(".card-machine-id")].map(e => e.textContent);
|
||||
const bannerActive = () => document.getElementById("offline-banner").classList.contains("active");
|
||||
const inst = () => FakeWebSocket.instances;
|
||||
|
||||
setTimeout(() => {
|
||||
assert("1. polaczenie nawiazane", inst().length === 1 && inst()[0].readyState === 1);
|
||||
inst()[0]._message({type:"machine_data", machine:"2.60", data:{status:"Alarm - awaria"}});
|
||||
inst()[0]._message({type:"machine_data", machine:"3.22", data:{status:"Automatyczny"}});
|
||||
}, 50);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("2. migawka: widok alarmowy z 2.60", alarmActive() && cards().join()==="2.60");
|
||||
inst()[0]._message({type:"machine_update", machine:"3.22", data:{status:"Alarm - przekroczony cykl"}});
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("3. update na zywo: dwie karty", alarmActive() && cards().join()==="2.60,3.22");
|
||||
inst()[0]._message({type:"machine_update", machine:"2.60", data:{status:"Automatyczny"}});
|
||||
inst()[0]._message({type:"machine_update", machine:"3.22", data:{status:"Automatyczny"}});
|
||||
}, 150);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("4. koniec alarmow: powrot do iframe", !alarmActive());
|
||||
inst()[0]._message({type:"machine_update", machine:"9.99", data:{status:"Alarm - obca"}});
|
||||
}, 200);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("5. maszyna spoza filtra ignorowana", !alarmActive());
|
||||
FakeWebSocket.autoOpen = false;
|
||||
inst()[0]._serverClose();
|
||||
}, 250);
|
||||
|
||||
// KLUCZOWY test regresji: po zamknięciu MUSI powstać nowe połączenie.
|
||||
setTimeout(() => {
|
||||
assert("6. reconnect zaplanowany i wykonany", inst().length >= 2);
|
||||
}, 5000);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("7. baner offline widoczny po 10 s", bannerActive());
|
||||
FakeWebSocket.autoOpen = true;
|
||||
inst()[inst().length-1]._open();
|
||||
inst()[inst().length-1]._message({type:"machine_data", machine:"4.09", data:{status:"Alarm - test"}});
|
||||
}, 16000);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("8. po powrocie: baner znika, alarm 4.09", !bannerActive() && cards().join()==="4.09");
|
||||
}, 17000);
|
||||
|
||||
setTimeout(() => { window.__przedWatchdog = inst().length; }, 18000);
|
||||
setTimeout(() => {
|
||||
assert("9. watchdog: cisza 100s wymusza nowe polaczenie", inst().length > window.__przedWatchdog);
|
||||
}, 125000);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("10. przeterminowany alarm znika z ekranu", !alarmActive());
|
||||
document.getElementById("wynik").textContent += "\\nKONIEC";
|
||||
}, 340000);
|
||||
"""
|
||||
|
||||
FAKE_WS = """
|
||||
class FakeWebSocket {
|
||||
static instances = [];
|
||||
static autoOpen = true;
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = 0;
|
||||
FakeWebSocket.instances.push(this);
|
||||
if (FakeWebSocket.autoOpen) setTimeout(() => this._open(), 5);
|
||||
}
|
||||
_open() {
|
||||
if (this.readyState !== 0) return;
|
||||
this.readyState = 1;
|
||||
this.onopen && this.onopen();
|
||||
}
|
||||
_message(obj) {
|
||||
this.onmessage && this.onmessage({ data: JSON.stringify(obj) });
|
||||
}
|
||||
_serverClose() {
|
||||
if (this.readyState === 3) return;
|
||||
this.readyState = 3;
|
||||
this.onclose && this.onclose();
|
||||
}
|
||||
close() {
|
||||
if (this.readyState === 3) return;
|
||||
this.readyState = 3;
|
||||
setTimeout(() => this.onclose && this.onclose(), 1);
|
||||
}
|
||||
}
|
||||
FakeWebSocket.OPEN = 1;
|
||||
window.WebSocket = FakeWebSocket;
|
||||
window.WebSocket.OPEN = 1;
|
||||
"""
|
||||
|
||||
|
||||
def zbuduj_harness() -> str:
|
||||
app = APP.read_text(encoding="utf-8")
|
||||
skrypt = re.search(r"<script>([\s\S]*)</script>", app).group(1)
|
||||
markup = re.search(r"<body>([\s\S]*?)<script>", app).group(1)
|
||||
# iframe bez realnego adresu — test działa offline
|
||||
markup = re.sub(r'src="http://[^"]*"', 'src="about:blank"', markup)
|
||||
return f"""<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
#alarm-view{{display:none}} #alarm-view.active{{display:flex}}
|
||||
#offline-banner{{display:none}} #offline-banner.active{{display:block}}
|
||||
</style></head><body>
|
||||
{markup}
|
||||
<pre id="wynik"></pre>
|
||||
<script>{FAKE_WS}</script>
|
||||
<script>{skrypt}</script>
|
||||
<script>{SCENARIUSZ}</script>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
def znajdz_chromium() -> str:
|
||||
for kandydat in (
|
||||
os.environ.get("CHROME_BIN"),
|
||||
shutil.which("chromium"),
|
||||
shutil.which("chromium-browser"),
|
||||
shutil.which("google-chrome"),
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
):
|
||||
if kandydat and Path(kandydat).exists():
|
||||
return kandydat
|
||||
sys.exit("Nie znaleziono Chromium/Chrome — ustaw CHROME_BIN.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
chrome = znajdz_chromium()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
harness = Path(tmp) / "harness.html"
|
||||
harness.write_text(zbuduj_harness(), encoding="utf-8")
|
||||
wynik = subprocess.run(
|
||||
[
|
||||
chrome,
|
||||
"--headless",
|
||||
"--disable-gpu",
|
||||
"--no-sandbox",
|
||||
"--virtual-time-budget=350000",
|
||||
"--dump-dom",
|
||||
f"file://{harness}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=180,
|
||||
)
|
||||
m = re.search(r'<pre id="wynik">([\s\S]*?)</pre>', wynik.stdout)
|
||||
if not m:
|
||||
print("BŁĄD: brak wyników w DOM — harness nie wystartował?")
|
||||
print(wynik.stdout[:500])
|
||||
return 2
|
||||
linie = [l for l in m.group(1).strip().splitlines() if l]
|
||||
for linia in linie:
|
||||
print(linia)
|
||||
if "KONIEC" not in linie:
|
||||
print("BŁĄD: scenariusz nie dobiegł końca (za mały virtual-time-budget?)")
|
||||
return 2
|
||||
return 1 if any(l.startswith("FAIL") for l in linie) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user