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
|
# 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
|
## 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 |
|
| 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 |
|
| 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
|
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
|
||||||
- Karty maszyn wypełniają cały dostępny ekran w układzie **siatki N×M** (możliwie kwadratowej).
|
bezpośrednie źródło) — przy zerwanym WebSockecie pada wyłącznie nasza warstwa
|
||||||
- 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.
|
wykrywania alarmów. Jedyny wyjątek: karty na ekranie alarmowym, jeśli przerwa
|
||||||
- Pod numerem wyświetlany jest pełny tekst statusu.
|
nastąpi w trakcie alarmu — mogą być nieświeże do 5 min, potem znikają.
|
||||||
- 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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Konfiguracja
|
## 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
|
Oba kluczowe parametry można też nadpisać w adresie strony — wygodne przy
|
||||||
const MACHINES = [
|
konfiguracji kiosku bez edycji pliku:
|
||||||
"1.64", "1.76", "2.60", ...
|
|
||||||
];
|
```
|
||||||
|
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
|
### Format wiadomości
|
||||||
const WS_BASE = "ws://luport.local/ws/als-data/";
|
|
||||||
```
|
|
||||||
|
|
||||||
Dla każdej maszyny otwierane jest połączenie pod adresem `WS_BASE + id + "/"`,
|
Strona obsługuje dwa typy ramek — obydwa tak samo (liczy się najnowszy stan):
|
||||||
np. `ws://luport.local/ws/als-data/2.60/`.
|
|
||||||
|
|
||||||
### Format wiadomości WebSocket
|
- `machine_data` — migawka wysyłana przez serwer zaraz po połączeniu,
|
||||||
|
- `machine_update` — aktualizacje na żywo.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "machine_update",
|
"type": "machine_update",
|
||||||
"machine": "2.60",
|
"machine": "2.60",
|
||||||
"data": {
|
"data": { "status": "Automatyczny", "orders": ["202605350"], "progress": 25, "...": "..." },
|
||||||
"machine": "2.60",
|
"timestamp": "2026-07-31T12:00:32"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
1. **Reconnect z wykładniczym odstępem i jitterem** (1 s → 30 s). Odstęp wraca
|
||||||
const RECONNECT_MS = 5000;
|
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
|
## Tryb debug
|
||||||
|
|
||||||
Służy do podglądu ekranu alarmowego bez rzeczywistych danych WS.
|
Podgląd ekranu alarmowego bez danych i bez połączeń WS:
|
||||||
|
|
||||||
### Przez URL
|
|
||||||
|
|
||||||
Otwórz stronę z parametrem `?debug`:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
als_alert_screen.html?debug
|
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`.
|
albo w konsoli przeglądarki: `DEBUG_ALARM = true; updateDisplay();`
|
||||||
|
(powrót: `DEBUG_ALARM = false; updateDisplay();`).
|
||||||
### 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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wymagania
|
## 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+).
|
- Chromium/Chrome 105+ (CSS Container Queries, `min()`) — kiosk działa na
|
||||||
- Dostęp sieciowy do `luport.local` (WebSocket) oraz `diluals31` (iframe).
|
Chromium pod Debianem.
|
||||||
|
- Dostęp sieciowy do LuPort (WebSocket) oraz infoterminala ALS (iframe).
|
||||||
|
|||||||
+279
-154
@@ -17,27 +17,42 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── NORMAL VIEW ──────────────────────────────── */
|
/* ── 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 {
|
#normal-view {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── ALARM VIEW ──────────────────────────────── */
|
/* ── ALARM VIEW ──────────────────────────────── */
|
||||||
#alarm-view {
|
#alarm-view {
|
||||||
display: none;
|
position: absolute;
|
||||||
flex-direction: column;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: #0d0000;
|
background: #0d0000;
|
||||||
overflow: hidden;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
#alarm-view.active {
|
#alarm-view.active {
|
||||||
display: flex;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alarm-header {
|
.alarm-header {
|
||||||
@@ -130,68 +145,68 @@
|
|||||||
text-shadow: 0 0 24px rgba(255, 0, 0, 0.6);
|
text-shadow: 0 0 24px rgba(255, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-status {
|
/* ── OFFLINE INDICATOR ───────────────────────── */
|
||||||
font-size: min(
|
/* Widoczna z daleka informacja o utracie łączności z LuPort — CIENKA
|
||||||
calc(7vw / var(--cols, 1)),
|
pulsująca RAMKA wokół całego ekranu plus mała plakietka w rogu.
|
||||||
calc(7vh / var(--rows, 1)),
|
Wcześniejsza pełnoekranowa belka u góry zasłaniała dane ALS
|
||||||
5cqw
|
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;
|
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;
|
letter-spacing: 0.05em;
|
||||||
}
|
pointer-events: none;
|
||||||
.info-value {
|
z-index: 9999;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── STATUS BAR ──────────────────────────────── */
|
/* ── STATUS BAR ──────────────────────────────── */
|
||||||
#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;
|
position: fixed;
|
||||||
bottom: 6px;
|
bottom: 6px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@@ -222,136 +237,236 @@
|
|||||||
<div id="machines-grid"></div>
|
<div id="machines-grid"></div>
|
||||||
</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>
|
<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 = [
|
const MACHINES = [
|
||||||
"1.64",
|
"1.64", "1.76", "2.60", "2.63", "2.67", "2.72", "2.74",
|
||||||
"1.76",
|
"3.01", "3.04", "3.08", "3.22", "3.26", "3.51", "3.55",
|
||||||
"2.60",
|
"3.56", "3.59", "3.61", "3.66", "3.69", "3.70", "3.71",
|
||||||
"2.63",
|
"3.73", "3.75", "4.05", "4.09", "4.14", "4.52", "4.53",
|
||||||
"2.67",
|
"4.57", "4.58", "4.62", "4.65", "4.68", "5.29", "5.54",
|
||||||
"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/";
|
// Reconnect: wykładniczy odstęp z losowym rozrzutem, żeby po awarii
|
||||||
const RECONNECT_MS = 5000;
|
// 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 ────────────────────────────────────────────────────────────
|
// Watchdog martwego połączenia. Przeglądarka NIE wystawia ping/pong do
|
||||||
// Otwórz stronę z ?debug aby wymusić widok alarmowy z przykładowymi danymi.
|
// JS — gdy trasa sieciowa umrze bez zamknięcia TCP, `onclose` może nie
|
||||||
// Możesz też wpisać w konsoli: DEBUG_ALARM = true; updateDisplay();
|
// 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");
|
let DEBUG_ALARM = new URLSearchParams(location.search).has("debug");
|
||||||
|
|
||||||
const DEBUG_MACHINES = ["2.60", "3.22", "4.09"];
|
|
||||||
const DEBUG_DATA = {
|
const DEBUG_DATA = {
|
||||||
"2.60": { machine: "2.60", status: "Alarm - brak materiału" },
|
"2.60": { machine: "2.60", status: "Alarm - brak materiału" },
|
||||||
"3.22": { machine: "3.22", status: "Alarm - awaria" },
|
"3.22": { machine: "3.22", status: "Alarm - awaria" },
|
||||||
"4.09": { machine: "4.09", status: "Alarm - przekroczony cykl" },
|
"4.09": { machine: "4.09", status: "Alarm - przekroczony cykl" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const machineData = {}; // id → last data payload
|
// ══ Stan ══════════════════════════════════════════════════════════════
|
||||||
const wsStatus = {}; // id → 'connected' | 'error' | 'disconnected'
|
|
||||||
const wsMap = {}; // id → WebSocket instance
|
|
||||||
|
|
||||||
// ── 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) {
|
const machineFilter = MACHINES.length ? new Set(MACHINES) : null;
|
||||||
if (wsMap[id]) {
|
|
||||||
|
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 {
|
try {
|
||||||
wsMap[id].close();
|
ws.onclose = null; // stare gniazdo nie może zaplanować DRUGIEGO reconnectu
|
||||||
|
ws.close();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(WS_BASE + id + "/");
|
ws = new WebSocket(wsUrlWithToken());
|
||||||
wsMap[id] = ws;
|
|
||||||
wsStatus[id] = "connecting";
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
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();
|
refreshStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
|
lastMessageAt = Date.now();
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(ev.data);
|
const msg = JSON.parse(ev.data);
|
||||||
if (msg.type === "machine_update" && msg.data) {
|
// "machine_data" = migawka po połączeniu, "machine_update" = strumień
|
||||||
machineData[id] = { ...msg.data, _ts: msg.timestamp };
|
// 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();
|
updateDisplay();
|
||||||
|
refreshStatus();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore malformed frames */
|
/* ignoruj uszkodzone ramki */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onclose = () => {
|
||||||
wsStatus[id] = "error";
|
// 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();
|
refreshStatus();
|
||||||
|
reconnectTimer = setTimeout(connect, nextReconnectDelay());
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
// Celowo brak ws.onerror: po błędzie przeglądarka i tak wywołuje
|
||||||
wsStatus[id] = "disconnected";
|
// onclose, a osobny handler był w starej wersji tylko drugim miejscem,
|
||||||
refreshStatus();
|
// z którego dało się rzucić wyjątkiem.
|
||||||
setTimeout(() => connect(id), RECONNECT_MS);
|
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) {
|
// Watchdog + odświeżenie widoku (przeterminowane dane, pasek offline)
|
||||||
if (DEBUG_ALARM) return true; // w trybie debug wszystkie wstrzyknięte maszyny to alarmy
|
// 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 (
|
return (
|
||||||
data &&
|
entry &&
|
||||||
typeof data.status === "string" &&
|
entry.data &&
|
||||||
data.status.toLowerCase().includes("alarm")
|
typeof entry.data.status === "string" &&
|
||||||
|
entry.data.status.toLowerCase().includes("alarm")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDisplay() {
|
function isFresh(entry) {
|
||||||
const alarms = Object.entries(machineData)
|
return entry && Date.now() - entry.receivedAt < STALE_MS;
|
||||||
.filter(([, d]) => isAlarm(d))
|
}
|
||||||
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
|
||||||
|
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 alarmView = document.getElementById("alarm-view");
|
||||||
const countEl = document.getElementById("alarm-count");
|
const countEl = document.getElementById("alarm-count");
|
||||||
|
|
||||||
if (alarms.length > 0) {
|
if (alarms.length > 0) {
|
||||||
normalView.style.display = "none";
|
|
||||||
alarmView.classList.add("active");
|
alarmView.classList.add("active");
|
||||||
countEl.textContent =
|
countEl.textContent =
|
||||||
alarms.length === 1 ? "1 maszyna" : alarms.length + " maszyny";
|
alarms.length === 1 ? "1 maszyna" : alarms.length + " maszyny";
|
||||||
renderCards(alarms);
|
renderCards(alarms);
|
||||||
} else {
|
} else {
|
||||||
normalView.style.display = "block";
|
|
||||||
alarmView.classList.remove("active");
|
alarmView.classList.remove("active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,34 +483,44 @@
|
|||||||
grid.style.setProperty("--cols", cols);
|
grid.style.setProperty("--cols", cols);
|
||||||
grid.style.setProperty("--rows", rows);
|
grid.style.setProperty("--rows", rows);
|
||||||
|
|
||||||
alarms.forEach(([id, d]) => {
|
alarms.forEach(([id, entry]) => {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "machine-card";
|
card.className = "machine-card";
|
||||||
card.innerHTML = `
|
const machineId = document.createElement("div");
|
||||||
<div class="card-machine-id">${d.machine ?? id}</div>
|
machineId.className = "card-machine-id";
|
||||||
<!--- <div class="card-status">${d.status ?? "ALARM"}</div> -->
|
machineId.textContent = entry.data.machine ?? id;
|
||||||
`;
|
card.appendChild(machineId);
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshStatus() {
|
function refreshStatus() {
|
||||||
const connected = Object.values(wsStatus).filter(
|
// Osłona zamiast założenia, że element istnieje — dokładnie ten brak
|
||||||
(s) => s === "connected",
|
// osłony ubijał reconnect w poprzedniej wersji (patrz komentarz przy
|
||||||
).length;
|
// #status-bar w HTML).
|
||||||
document.getElementById("status-bar").textContent =
|
const bar = document.getElementById("status-bar");
|
||||||
`WS: ${connected} / ${MACHINES.length}`;
|
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) {
|
if (DEBUG_ALARM) {
|
||||||
DEBUG_MACHINES.forEach((id) => {
|
const bar = document.getElementById("status-bar");
|
||||||
machineData[id] = DEBUG_DATA[id];
|
if (bar) bar.textContent = "[DEBUG] widok alarmowy";
|
||||||
});
|
|
||||||
document.getElementById("status-bar").textContent =
|
|
||||||
"[DEBUG] widok alarmowy";
|
|
||||||
} else {
|
} else {
|
||||||
MACHINES.forEach(connect);
|
connect();
|
||||||
}
|
}
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
</script>
|
</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