Compare commits
11 Commits
7cf063431d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e694f6b74 | |||
| 89155c3f2a | |||
| d97d139d52 | |||
| 199e71327f | |||
| c893b5e975 | |||
| 9d196fcfca | |||
| 90fd95e978 | |||
| 4b7b54d4d3 | |||
| 53c7423c5e | |||
| b0a66ed982 | |||
| 7171e5ea09 |
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
@@ -1,145 +1,176 @@
|
||||
# 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ńczowa pulsująca ramka wokół ekranu + plakietka w rogu (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"`.
|
||||
|
||||
---
|
||||
Wskaźnik offline to cienka pulsująca RAMKA wokół całego ekranu plus mała
|
||||
plakietka w rogu — niczego nie zasłania (pełnoekranowa belka zakrywała dane
|
||||
ALS w iframe). Pomarańcz to celowo INNY kolor niż czerwień alarmów.
|
||||
|
||||
## 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 |
|
||||
Komunikat mówi o tym, co FAKTYCZNIE nie działa: **powiadamianie o alarmach**.
|
||||
Dane infoterminala w iframe są aktualne niezależnie od LuPorta (iframe to
|
||||
bezpośrednie źródło) — przy zerwanym WebSockecie pada wyłącznie nasza warstwa
|
||||
wykrywania alarmów. Jedyny wyjątek: karty na ekranie alarmowym, jeśli przerwa
|
||||
nastąpi w trakcie alarmu — mogą być nieświeże do 5 min, potem znikają.
|
||||
|
||||
---
|
||||
|
||||
## 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ć ramkę offline (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"
|
||||
"type": "machine_update",
|
||||
"machine": "2.60",
|
||||
"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.** Pulsująca ramka offline świeci CIĄGLE
|
||||
przez całą przerwę (znacznik początku przerwy ustawiany tylko przy
|
||||
przejściu połączone→rozłączone — reset przy każdej nieudanej próbie
|
||||
powodował cykliczne miganie). Ekran nigdy nie pokazuje po cichu
|
||||
nieświeżego obrazu.
|
||||
|
||||
Uwaga: do czasu wdrożenia NOWEGO LuPorta ramka będzie świecić stale —
|
||||
stary system nie ma kanału zbiorczego ani uwierzytelniania WS, więc nowa
|
||||
strona nie ma się z czym połączyć. To oczekiwane; po przełączeniu
|
||||
serwera na nowy system strona wstaje bez żadnych zmian.
|
||||
|
||||
---
|
||||
|
||||
## 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 (11 asercji: migawka, aktualizacje, reconnect, ciągłość
|
||||
wskaźnika offline podczas długiej przerwy, watchdog, wygasanie danych)
|
||||
jedzie na wirtualnym czasie Chromium — całość trwa kilkanaście sekund mimo
|
||||
symulowania ~8 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).
|
||||
|
||||
+279
-154
@@ -17,27 +17,42 @@
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ── 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;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
border: none;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── ALARM VIEW ──────────────────────────────── */
|
||||
#alarm-view {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0d0000;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
#alarm-view.active {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.alarm-header {
|
||||
@@ -130,68 +145,68 @@
|
||||
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
|
||||
);
|
||||
/* ── OFFLINE INDICATOR ───────────────────────── */
|
||||
/* Widoczna z daleka informacja o utracie łączności z LuPort — CIENKA
|
||||
pulsująca RAMKA wokół całego ekranu plus mała plakietka w rogu.
|
||||
Wcześniejsza pełnoekranowa belka u góry zasłaniała dane ALS
|
||||
w iframe (zgłoszone 2026-07-31) — ramka niczego nie zakrywa,
|
||||
a z daleka widać ją równie dobrze. POMARAŃCZOWA, nie czerwona:
|
||||
czerwień jest zarezerwowana dla alarmów maszyn. Ramka mówi
|
||||
„powiadamianie o alarmach nie działa" — dane samego infoterminala
|
||||
w iframe są aktualne niezależnie od LuPorta (to bezpośrednie
|
||||
źródło), pada wyłącznie nasza warstwa wykrywania alarmów. Utrata
|
||||
łączności MA być widoczna z hali — poprzednia wersja strony traciła
|
||||
połączenia po cichu (README, Historia). */
|
||||
#offline-indicator {
|
||||
display: none;
|
||||
}
|
||||
#offline-indicator.active {
|
||||
display: block;
|
||||
}
|
||||
#offline-frame {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
border: 6px solid #ffaa33;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
animation: offlinePulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes offlinePulse {
|
||||
0%,
|
||||
100% {
|
||||
border-color: #ffaa33;
|
||||
box-shadow: inset 0 0 20px rgba(255, 170, 51, 0.25);
|
||||
}
|
||||
50% {
|
||||
border-color: #7a4a00;
|
||||
box-shadow: inset 0 0 6px rgba(255, 170, 51, 0.1);
|
||||
}
|
||||
}
|
||||
#offline-chip {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
background: #7a4a00;
|
||||
color: #ffd9a0;
|
||||
border: 1px solid #ffaa33;
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #ff9999;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
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;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.info-value {
|
||||
font-size: clamp(11px, 1.1vw, 16px);
|
||||
color: #ddcccc;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.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;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* ── STATUS BAR ──────────────────────────────── */
|
||||
#status-bar {
|
||||
/* UKRYTY NA ŻYCZENIE — nakładał się na dane informacyjne ALS
|
||||
w iframe. Ukrycie WYŁĄCZNIE przez CSS: usunięcie/zakomentowanie
|
||||
elementu z HTML to dokładnie ten błąd, który ubijał reconnect
|
||||
w starej wersji (TypeError w onclose — patrz README, Historia).
|
||||
Do diagnostyki: usuń linię `display: none` albo w konsoli
|
||||
document.getElementById("status-bar").style.display = "block". */
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 6px;
|
||||
right: 10px;
|
||||
@@ -222,136 +237,236 @@
|
||||
<div id="machines-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- <div id="status-bar">WS: 0 / 0</div> -->
|
||||
<div id="offline-indicator">
|
||||
<div id="offline-frame"></div>
|
||||
<div id="offline-chip">⚠ BRAK POŁĄCZENIA Z LUPORT — powiadamianie o alarmach nie działa</div>
|
||||
</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" },
|
||||
};
|
||||
|
||||
const machineData = {}; // id → last data payload
|
||||
const wsStatus = {}; // id → 'connected' | 'error' | 'disconnected'
|
||||
const wsMap = {}; // id → WebSocket instance
|
||||
// ══ Stan ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ── WebSocket management ──────────────────────────────────────────────────
|
||||
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();
|
||||
|
||||
function connect(id) {
|
||||
if (wsMap[id]) {
|
||||
const machineFilter = MACHINES.length ? new Set(MACHINES) : null;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ══ Połączenie ════════════════════════════════════════════════════════
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
lastMessageAt = Date.now();
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === "machine_update" && msg.data) {
|
||||
machineData[id] = { ...msg.data, _ts: msg.timestamp };
|
||||
// "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";
|
||||
ws.onclose = () => {
|
||||
// Znacznik początku przerwy ustawiany WYŁĄCZNIE przy przejściu
|
||||
// połączone->rozłączone. Reset przy każdej NIEUDANEJ próbie
|
||||
// reconnectu (jak wcześniej) zerował licznik ciszy co próbę i przy
|
||||
// odstępie ponowień > 10 s wskaźnik offline cyklicznie znikał
|
||||
// i wracał (zgłoszone 2026-07-31: „pasek pojawia się i znika") —
|
||||
// zamiast świecić ciągle przez całą przerwę.
|
||||
if (wsConnected) disconnectedSince = Date.now();
|
||||
wsConnected = false;
|
||||
clearTimeout(stableTimer);
|
||||
refreshStatus();
|
||||
reconnectTimer = setTimeout(connect, nextReconnectDelay());
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
wsStatus[id] = "disconnected";
|
||||
refreshStatus();
|
||||
setTimeout(() => connect(id), RECONNECT_MS);
|
||||
};
|
||||
// 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))
|
||||
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
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 normalView = document.getElementById("normal-view");
|
||||
const alarmView = document.getElementById("alarm-view");
|
||||
const countEl = document.getElementById("alarm-count");
|
||||
|
||||
if (alarms.length > 0) {
|
||||
normalView.style.display = "none";
|
||||
alarmView.classList.add("active");
|
||||
countEl.textContent =
|
||||
alarms.length === 1 ? "1 maszyna" : alarms.length + " maszyny";
|
||||
renderCards(alarms);
|
||||
} else {
|
||||
normalView.style.display = "block";
|
||||
alarmView.classList.remove("active");
|
||||
}
|
||||
}
|
||||
@@ -368,34 +483,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 indicator = document.getElementById("offline-indicator");
|
||||
if (indicator) {
|
||||
const offlineLongEnough =
|
||||
!wsConnected && Date.now() - disconnectedSince > OFFLINE_BANNER_AFTER_MS;
|
||||
indicator.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 {
|
||||
MACHINES.forEach(connect);
|
||||
connect();
|
||||
}
|
||||
updateDisplay();
|
||||
</script>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,228 @@
|
||||
#!/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 offline = () => document.getElementById("offline-indicator").classList.contains("active");
|
||||
const inst = () => FakeWebSocket.instances;
|
||||
const otwarty = () => inst().find(w => w.readyState === 1);
|
||||
|
||||
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());
|
||||
// serwer pada NA DLUGO: kazda kolejna proba polaczenia jest odrzucana
|
||||
FakeWebSocket.autoOpen = false;
|
||||
inst()[0]._serverClose();
|
||||
}, 250);
|
||||
|
||||
// KLUCZOWY test regresji starego bledu: po zamknieciu MUSI powstac nowa proba.
|
||||
setTimeout(() => {
|
||||
assert("6. reconnect zaplanowany i wykonany", inst().length >= 2);
|
||||
}, 5000);
|
||||
|
||||
// ANTY-MIGANIE (regresja 2026-07-31): przy DLUGIEJ przerwie wskaznik ma
|
||||
// swiecic CIAGLE. Blad resetowal disconnectedSince przy kazdej nieudanej
|
||||
// probie, wiec przy odstepie ponowien > 10 s wskaznik cyklicznie znikal.
|
||||
// Probkujemy co 500 ms przez 90 s ciaglej awarii — z bledem ~30% probek
|
||||
// bylo "zgaszonych", po poprawce wszystkie musza byc aktywne.
|
||||
window.__probki = { aktywne: 0, wszystkie: 0 };
|
||||
const probkowanie = setInterval(() => {
|
||||
window.__probki.wszystkie++;
|
||||
if (offline()) window.__probki.aktywne++;
|
||||
}, 500);
|
||||
setTimeout(() => { window.__probki.aktywne = 0; window.__probki.wszystkie = 0; }, 20000);
|
||||
setTimeout(() => {
|
||||
clearInterval(probkowanie);
|
||||
const p = window.__probki;
|
||||
assert("7. wskaznik offline swieci CIAGLE przez cala przerwe (" +
|
||||
p.aktywne + "/" + p.wszystkie + " probek)", p.wszystkie > 100 && p.aktywne === p.wszystkie);
|
||||
// serwer wraca — najblizsza zaplanowana proba wstanie
|
||||
FakeWebSocket.autoOpen = true;
|
||||
}, 111000);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("8. po powrocie serwera: polaczono, wskaznik zgaszony", !!otwarty() && !offline());
|
||||
otwarty() && otwarty()._message({type:"machine_data", machine:"4.09", data:{status:"Alarm - test"}});
|
||||
}, 155000);
|
||||
|
||||
setTimeout(() => {
|
||||
assert("9. alarm po powrocie widoczny", alarmActive() && cards().join()==="4.09");
|
||||
window.__przedWatchdog = inst().length;
|
||||
}, 156000);
|
||||
|
||||
// cisza > 90 s -> watchdog ubija polaczenie, reconnect wstaje od reki
|
||||
setTimeout(() => {
|
||||
assert("10. watchdog: cisza wymusza nowe polaczenie", inst().length > window.__przedWatchdog);
|
||||
}, 270000);
|
||||
|
||||
// dane 4.09 z t=155 s bez odswiezenia -> po 5 min znikaja z ekranu
|
||||
setTimeout(() => {
|
||||
assert("11. przeterminowany alarm znika z ekranu", !alarmActive());
|
||||
document.getElementById("wynik").textContent += "\\nKONIEC";
|
||||
}, 470000);
|
||||
"""
|
||||
|
||||
FAKE_WS = """
|
||||
class FakeWebSocket {
|
||||
static instances = [];
|
||||
// true = polaczenie wstaje (serwer dziala)
|
||||
// false = polaczenie ODRZUCANE po chwili (serwer lezy / zly adres) —
|
||||
// przegladarka w tej sytuacji wola onclose, wiec fake tez musi,
|
||||
// inaczej nie da sie przetestowac dlugiej przerwy z ponowieniami.
|
||||
static autoOpen = true;
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = 0;
|
||||
FakeWebSocket.instances.push(this);
|
||||
if (FakeWebSocket.autoOpen) setTimeout(() => this._open(), 5);
|
||||
else setTimeout(() => this._serverClose(), 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-indicator{{display:none}} #offline-indicator.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=480000",
|
||||
"--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