Compare commits

...

9 Commits

Author SHA1 Message Date
sases 5e694f6b74 Komunikat offline mówi, co faktycznie nie działa: powiadamianie o alarmach
"Dane mogą być nieaktualne" wprowadzało w błąd — iframe pokazuje infoterminal
ALS bezpośrednio, więc jego dane są aktualne niezależnie od LuPorta. Przy
zerwanym WebSockecie pada wyłącznie nasza warstwa wykrywania alarmów i to
komunikat ma mówić. (Niuans opisany w README: karty na ekranie alarmowym przy
przerwie w trakcie alarmu mogą być nieświeże do 5 min, potem znikają.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 08:19:28 +02:00
sases 89155c3f2a Wskaźnik offline: ramka zamiast belki + naprawa cyklicznego migania
Dwa zgłoszenia z kiosku (2026-07-31):

1. BELKA ZASŁANIAŁA DANE ALS. Pełnoekranowy pasek u góry zajmował pas
   ekranu i obcinał dane informacyjne w iframe. Zastąpiony cienką pulsującą
   RAMKĄ wokół całego ekranu (pointer-events: none, niczego nie zakrywa)
   plus małą plakietką w rogu. Z daleka widoczna równie dobrze.

2. WSKAŹNIK MIGAŁ zamiast świecić ciągle. disconnectedSince było resetowane
   przy KAŻDEJ nieudanej próbie reconnectu, nie tylko przy przejściu
   połączone->rozłączone — przy odstępie ponowień > 10 s (backoff dochodzi
   do 30 s) licznik ciszy zerował się co próbę i wskaźnik cyklicznie znikał
   i wracał. Teraz znacznik początku przerwy ustawiany wyłącznie przy
   przejściu ze stanu połączonego.

   Kontekst zgłoszenia: strona działa jeszcze przeciw STAREMU LuPortowi,
   który nie ma kanału zbiorczego ani uwierzytelniania WS — połączenie pada
   ciągle (oczekiwane do czasu wdrożenia; potem wstaje bez zmian), a błąd
   zamieniał tę ciągłą przerwę w miganie.

Testy: scenariusz 7 próbkuje wskaźnik co 500 ms przez 90 s ciągłej awarii
i wymaga 100% aktywnych próbek — z błędem aktywnych było 51/182 (72% czasu
zgaszony; dokładnie zgłoszone "pojawia się i znika"). Fałszywy WebSocket
odrzuca teraz połączenia jak przeglądarka (onclose po nieudanej próbie),
więc długa przerwa z ponowieniami jest testowalna. 11/11 scenariuszy PASS,
mutacja poprawki wywraca scenariusz 7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 08:16:36 +02:00
sases d97d139d52 Pasek statusu ukryty przez CSS (nakładał się na dane ALS w iframe)
Ukrycie WYŁĄCZNIE przez display:none — element zostaje w DOM. Usunięcie go
z HTML to dokładnie ten błąd, który w starej wersji ubijał reconnect
(TypeError w onclose; README, Historia). refreshStatus() i tak ma osłonę na
brak elementu, ale osłona jest siatką bezpieczeństwa, nie zaproszeniem.
Do diagnostyki: jedna linia CSS albo style.display="block" w konsoli.

Pomarańczowy pasek offline zostaje bez zmian — pojawia się tylko przy utracie
łączności, czyli dokładnie wtedy, gdy MA przykryć nieaktualne dane.

10/10 testów przechodzi (testy sprawdzają logikę i klasy, nie widoczność
paska, więc ukrycie ich nie dotyka).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 08:09:12 +02:00
sases 199e71327f 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>
2026-07-31 08:04:30 +02:00
admin c893b5e975 iframe optimalization 2026-06-22 14:49:42 +02:00
admin 9d196fcfca websocket connection fixes 2026-06-19 13:36:50 +02:00
admin 90fd95e978 DATA_STALE_MS - 2 minutes 2026-06-08 10:28:11 +02:00
admin 4b7b54d4d3 data timers 2026-06-08 09:55:59 +02:00
admin 53c7423c5e unicode char fix 2026-06-08 09:43:26 +02:00
5 changed files with 632 additions and 252 deletions
+1
View File
@@ -0,0 +1 @@
.DS_Store
+122 -91
View File
@@ -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 -159
View File
@@ -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,141 +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.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 = () => {
wsStatus[id] = "error";
refreshStatus();
};
ws.onclose = () => { ws.onclose = () => {
wsStatus[id] = "disconnected"; // Znacznik początku przerwy ustawiany WYŁĄCZNIE przy przejściu
// Usuwamy dane maszyny przy rozłączeniu nie możemy potwierdzić // połączone->rozłączone. Reset przy każdej NIEUDANEJ próbie
// aktualnego stanu, więc nie trzymamy przestarzałego alarmu w cache. // reconnectu (jak wcześniej) zerował licznik ciszy co próbę i przy
// Po ponownym połączeniu serwer wyśle aktualny stan. // odstępie ponowień > 10 s wskaźnik offline cyklicznie znikał
delete machineData[id]; // i wracał (zgłoszone 2026-07-31: „pasek pojawia się i znika") —
updateDisplay(); // zamiast świecić ciągle przez całą przerwę.
if (wsConnected) disconnectedSince = Date.now();
wsConnected = false;
clearTimeout(stableTimer);
refreshStatus(); refreshStatus();
setTimeout(() => connect(id), RECONNECT_MS); 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) { // 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");
} }
} }
@@ -373,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…";
} }
// ── Bootstrap ───────────────────────────────────────────────────────────── 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);
}
}
// ══ 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.
+228
View File
@@ -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())