Panel pytal o potwierdzenie natywnym `window.confirm`. Takie okno wyglada
inaczej w kazdej przegladarce i w kazdym systemie, wyskakuje przy gornej
krawedzi zamiast na srodku i pokazuje nad trescia adres domeny ("Komunikat
ze strony ..."). W panelu, ktory poza tym jest spojnym interfejsem, czyta sie
to jak blad strony, a nie jak pytanie aplikacji.
CLAUDE.md wymagalo juz, zeby kazde okno modalne uzywalo komponentu `Modal`,
i tlumaczylo przy otwartym mikrofonie, dlaczego nie `window.confirm`. Regula
obowiazywala w jednym miejscu, a w 42 innych nie.
HOOK, A NIE OKNO W KAZDYM MIEJSCU Z OSOBNA
Potwierdzen jest ponad czterdziesci. Wlasny stan i wlasny `Modal` do kazdego
oznaczalyby kilkanascie linii na jedno pytanie i te sama pewnosc, ze predzej
czy pozniej ktores bedzie wygladalo inaczej. `useConfirm` oddaje funkcje
o ksztalcie zblizonym do `confirm`, wiec zamiana jest jednolinijkowa. Wynik
przychodzi obietnica, bo okno musi sie najpierw wyrenderowac.
TRESC MOWI, CO SIE STANIE
Przy okazji poprawione komunikaty: zamiast "Czy na pewno?" pytania nazywaja
obiekt i skutek. Plik zniknie ze skladu instancji, odbiorca straci dostep do
API, razem z banerem przepadnie jego statystyka wraz z liczbami. Etykieta
przycisku nazywa czynnosc ("Usun nagranie"), a nie mowi "OK".
NIEODWRACALNE WYROZNIONE, ODWRACALNE NIE
`destructive` daje czerwony przycisk i ostrzezenie "Tej operacji nie da sie
cofnac". Odwolanie webinaru i dezaktywacja konta go NIE dostaja: sa
odwracalne, a straszenie tam, gdzie nic nie przepada, rozmydla sygnal tam,
gdzie strata jest prawdziwa. Zgodnie z reguła, ze usuniecie webinaru ma
podpowiadac odwolanie jako lagodniejsza alternatywe.
Sprawdzone `npx tsc -b --force` (pelne typowanie bez cache) i `npm run build`.
Przy okazji poprawione dwa em dash sprzed tej zmiany, w `ApiKeyListPage`
i `ProviderPicker`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WebSocket nie ma historii: zdarzenie wyslane, gdy panel byl rozlaczony,
przepada bezpowrotnie. Panel wracal wiec ze stanem sprzed zerwania i pokazywal
go jako aktualny.
Zdarzylo sie to realnie: backend zrestartowal sie przy wdrozeniu, uczestnik
poprosil o glos w czasie, gdy panel byl rozlaczony, a prowadzacy widzial
"Nikt nie prosi o glos" przy zgloszeniu czekajacym juz kilkadziesiat sekund.
Zgloszenie pojawilo sie dopiero po odswiezeniu strony. Objaw myli, bo panel
wyglada na dzialajacy, a jedyna wskazowka jest znikniecie znacznika
"polaczono".
Rozlaczenie nie jest sytuacja wyjatkowa: wystarczy restart backendu, chwilowa
utrata sieci albo uspienie laptopa. `reconnectDelay` wznawial polaczenie, ale
nikt nie dociagal tego, co umknelo w miedzyczasie.
`onConnect` odswieza teraz kolejke glosu, czat z pytaniami oraz tryby
mikrofonu i wideo. Przy pierwszym polaczeniu jest to zapytanie nadmiarowe,
przy kazdym nastepnym jedyne zrodlo prawdy.
`refetch` sa pominiete w zaleznosciach efektu swiadomie: TanStack Query daje
je stabilne, a dopisanie ich przelaczaloby WebSocket przy kazdym odswiezeniu
danych, czyli powodowalo dokladnie ten problem, ktoremu ta zmiana zapobiega.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KAZDE nagranie konczylo sie bledem 412 "Start signal not received" po okolo
minucie, przy poprawnie dzialajacym pokoju, szablonie osiagalnym po sieci
Dockera i wlasciwych danych MinIO.
Szablon wolal `window.START_RECORDING()`, czyli funkcje, ktorej nikt nigdy nie
przypisywal. Byla zadeklarowana wylacznie w `declare global` jako opcjonalna,
wiec warunek `tracks.length > 0 && window.START_RECORDING` byl ZAWSZE falszywy.
Nawet gdyby nie byl, wywolanie funkcji niczego by nie dalo.
Egress czyta **wyjscie konsoli przegladarki** i czeka na napis
`START_RECORDING`. Dziala bez interfejsu, wiec konsola jest jedynym kanalem,
ktorym strona moze cokolwiek powiedziec procesowi nadrzednemu. Mowi o tym
dokumentacja LiveKit: "The recorder waits for the page to log START_RECORDING
to the console".
`console.log` nie jest tu wiec debugowaniem i nie wolno go usunac przy
porzadkowaniu logow. Sprawdzone, ze napis przezywa minifikacje produkcyjna:
minifikator Vite domyslnie nie wycina `console.log`.
Powtorne wyslanie blokuje `useRef`, a nie kasowanie pola `window`.
Diagnoza zajela dluzej, bo objaw myli: zadanie dociera do Egressu i przechodzi
walidacje, kontener laczy sie z Redisem, szablon odpowiada HTTP 200, a Egress
wchodzi do pokoju i widnieje na liscie uczestnikow. Wszystko poza jedna linia
dziala.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nagrywania NIE DALO SIE URUCHOMIC. Backend mial `startRecording`
i `stopRecording`, Egress byl skonfigurowany i pisal prosto do MinIO, ale
brakowalo dwoch ogniw: przycisku w panelu i domkniecia stanu po zakonczeniu.
`completeRecording` nie bylo wolane z ZADNEGO miejsca w kodzie.
PRZYCISK W PANELU PROWADZENIA
Obok trybow mikrofonu i wideo, bo tam prowadzacy podejmuje decyzje o sali.
Osobno od nich, bo nagrywanie nie dotyczy uprawnien uczestnikow: to decyzja
organizatora o utrwaleniu szkolenia. Start bez potwierdzenia (nagrywanie
niczego nie ujawnia i da sie je zatrzymac), stop z potwierdzeniem, bo tego
fragmentu nie nagra sie drugi raz.
Stan czytany jest przy wejsciu z listy nagran, nie tylko trzymany lokalnie:
prowadzacy moze odswiezyc strone albo wrocic z innego urzadzenia i musi
zobaczyc, ze nagranie trwa, zamiast zaczynac drugie rownolegle. Sluzy do tego
`providerRecordingId` dopisane do `RecordingView`.
DOMYKANIE PRZEZ ODPYTYWANIE, NIE WEBHOOK
LiveKit podpisuje webhooki tokenem JWT, ktorego `VideoWebhookController` nie
rozpoznaje: przyjmuje wylacznie wspolny sekret w naglowku, tak jak Daily.
Odpytywanie jest przy tym ODPORNIEJSZE: zgloszenie, ktore nie dojdzie (restart
backendu w zlej chwili, awaria sieci), zostawia wpis w stanie DOWNLOADING na
zawsze, a zadanie w tle samo to nadrabia. Opoznienie do minuty nie ma
znaczenia, bo montaz pliku po stronie Egress i tak trwa dluzej.
EGRESS_COMPLETE domyka wpis rozmiarem i czasem trwania, FAILED/ABORTED/
LIMIT_REACHED oznacza go jako nieudany Z POWODEM: wpis zostaje w bazie, zeby
administrator widzial, ze proba byla i czym sie skonczyla. Zadanie nie rusza,
gdy dostawca nie jest LiveKit, i nie oznacza nagrania jako nieudanego, gdy
Egress zwyczajnie nie zna juz zadania: brak informacji to nie to samo co blad.
`reconcileRecordings` jest nietransakcyjne (wola serwer po sieci) i domyka
kazdy wpis przez `@Lazy self`: samowywolanie omija proxy Springa, a wtedy
zapis odbywa sie bez transakcji i stan nagrania nie zmienia sie wcale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Przycisk w sali, ktory kazdy z wlasna kamera wlacza i wylacza sam,
w trakcie spotkania. Pokazuje sie wylacznie osobie z wlaczona kamera,
wiec uczestnik bez prawa publikacji obrazu nigdy go nie zobaczy.
`@livekit/track-processors` wchodzi jako nowa zaleznosc, poza lista
z CLAUDE.md: uzgodnione. Ladowany leniwie, bo ciagnie za soba MediaPipe:
import statyczny doliczalby kilka MB do pakietu KAZDEGO wejscia na sale,
takze uczestnikom bez kamery. Sprawdzone na zbudowanym pakiecie: MediaPipe
siedzi w osobnym chunku, glowny pakiet nie urosl.
Pliki modelu ida z WLASNEJ domeny, nie z CDN: instancja w sieci zamknietej
ma miec rozmycie tak samo, a CSP nie musi dopuszczac obcych hostow.
Model (244 kB) lezy w repozytorium, a zestaw WASM kopiuje sie przy budowaniu
obrazu z `node_modules`: wazy kilkanascie MB i musi odpowiadac wersji
pakietu, wiec w repozytorium puchlby w historii i po cichu rozjezdzal sie
przy kazdej aktualizacji.
Podmiany tla na obraz swiadomie NIE ma. Segmentacja obcina wlosy i dlonie
przy slabym swietle, a kafelek wideo jest za maly, zeby cokolwiek na tle
bylo czytelne. Reklama ma wlasne miejsce w banerach, ktore sa przy tym
mierzone.
`VideoConference` zostaje nietkniety: przycisk jest obok, wiec aktualizacja
biblioteki nie wymaga odtwarzania niczego u nas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wylaczenie "wideo dla wszystkich" przy wlaczonym otwartym mikrofonie
wyciszalo WSZYSTKICH uczestnikow, takze mowiacych w tej chwili. Zdarzylo
sie to na zywo i zaklocilo spotkanie.
Zlozyly sie na to dwie rzeczy:
1. Stan `voiceState` byl trojstopniowy (`none` / `granted` / `video`)
i mieszal dwa NIEZALEZNE przelaczniki prowadzacego. Wideo przykrywalo
informacje o mikrofonie, wiec jego wylaczenie wygladalo jak zmiana
uprawnien, chociaz prawo do mikrofonu zostawalo.
2. `key={join.token}` przemontowywal sale przy KAZDYM nowym tokenie,
a przemontowanie jest ponownym wejsciem: provider startuje z
`audio={false}`, wiec kazda opublikowana sciezka przepada. Token jest
przy tym zawsze inny, bo JWT niesie czas wydania.
Sam token byl poprawny: rola SPEAKER zachowuje `canPublishAudio`.
Klucz sklada teraz `canPublishAudio` i `canPublishVideo`, czyli faktyczne
uprawnienia tego uczestnika wyliczone przez backend z roli, obu trybow
i przyznanego glosu. Zmiana trybu, ktora go nie dotyczy, nie rusza jego sali.
Do tego `initialAudio`/`initialVideo`: urzadzenia wlaczone przed
przemontowaniem wracaja wlaczone, wiec przemontowanie nieuniknione (realna
zmiana uprawnien) tez nie wycisza mowiacego. Kamera wraca wylacznie wtedy,
gdy uczestnik nadal ma do niej prawo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`PublicTaskOfferController` szukal tokenu wylacznie w `registrations`,
a prelegent tam nie istnieje: nie przechodzi przez formularz zapisu i ma
wlasny token w `webinar_speakers`. Kazdy endpoint sali odpowiadal mu 404.
Objawialo sie to tym, ze prowadzacy **nie widzial wlasnych zadan**: panel
pokazywal "Nie znaleziono zapisu" przy zadaniu ogloszonym minute wczesniej
i widocznym w panelu prowadzenia. Tak samo znikaly banery i nie liczyly sie
ich wyswietlenia. Zdarzylo sie to na zywo, w trakcie spotkania.
Nowa metoda `TaskService.viewer` obsluguje oba rodzaje tokenu, wzorowana na
`PublicInteractionController`, ktory robil to juz przy historii czatu.
Przelaczone sa WYLACZNIE odczyty: lista zadan, banery i ich zdarzenia.
Zapisy (zgloszenie do zadania, wyslanie oferty) zostaja przy `participant`,
bo prelegent nie zapisuje sie na wlasne zadanie.
`registrationId` rowne null jest tu poprawna wartoscia, nie brakiem danych:
`listViews` przewidywalo to od poczatku, a `banner_events.registration_id`
jest nullowalne wlasnie dla zdarzen od osob bez zapisu.
Front: `TasksTab` dostaje tryb `readOnly`, wiec prowadzacy widzi zadania
i liczbe zapisanych, ale bez przycisku zgloszenia.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prowadzacy widzial zakladke, ktora u niego nie mogla zadzialac: endpoint
`/offer-form` szuka tokenu w `registrations`, a prelegent tam nie istnieje
(nie przechodzi przez formularz zapisu i ma wlasny token w `webinar_speakers`).
Zadanie konczylo sie bledem 404, a panel zostawal na "Wczytywanie..." bez
konca, bo front nie obslugiwal bledu.
Zakladka i tak nie mialaby sensu: zapytanie ofertowe wysyla uczestnik, a
prowadzacy oglada zgloszenia w panelu prowadzenia.
Warunek jest w dwoch miejscach celowo: przy liscie zakladek i przy
renderowaniu tresci. Stan `tab` przezywa zmiane roli, wiec sama krotsza lista
nie wystarcza.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>