Domyslne `blurRadius: 10` z biblioteki jest za slabe do tego zastosowania.
Sprawdzone na zywo przy prowadzeniu z domu: przez rozmycie widac uklad pokoju,
zdjecia na polkach i przechodzace osoby, czyli dokladnie to, co ma zaslaniac.
25 daje tlo nierozpoznawalne. Obrys postaci zostaje przy tym czysty, bo maska
pochodzi z modelu segmentacji i nie zalezy od sily rozmycia: podnoszenie jej
nie psuje krawedzi. Kosztuje nieco mocy procesora, bo rozmycie liczy sie dla
kazdej klatki.
Model zostaje ten sam. `selfie_segmenter.tflite` to wariant "general" 256x256,
czyli DOKLADNIEJSZY z dwoch dostepnych; szybszego wariantu "landscape" uzywa
Google Meet. Przy nieruchomym prelegencie roznica w plynnosci jest
niezauwazalna, a jakosc obrysu ma tu wieksze znaczenie.
Wartosc w stalej z wyjasnieniem, zeby nastepna zmiana nie byla zgadywaniem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UKLAD
Okno korzystalo z komponentu `Modal`, ktory jest zbudowany pod FORMULARZE:
naglowek oddzielony kreska, przycisk zamkniecia w rogu, przewijana tresc
i stopka oddzielona druga kreska. Przy imporcie kontaktow to sluszne, ale przy
pytaniu na dwa zdania te same elementy rozdymaly okno: dwie linie podzialu
i krzyzyk zajmowaly wiecej miejsca niz sama tresc.
Okno potwierdzenia jest krotkie z natury, wiec dostaje uklad ciagly: ikona
ostrzezenia jako okragla plakietka OBOK tytulu zamiast osobnego paska, przyciski
tuz pod trescia. Krzyzyka nie ma, bo bylby trzecim sposobem powiedzenia "nie",
obok przycisku "Anuluj", klawisza Escape i klikniecia w tlo. Naklatka, Escape
i blokada przewijania tla zostaja takie same jak w `Modal`: powtarzamy
zachowanie, nie uklad.
OSTRZEZENIE PRZESTAJE BYC WSZEDZIE
Trzy operacje mialy `destructive`, choc niczego nie niszcza:
- zatrzymanie nagrywania: plik zostaje, nic nie przepada. Nieodwracalne jest
wylacznie to, ze nagranej chwili nie da sie powtorzyc,
- anulowanie zamowienia: zamowienie jest dokumentem finansowym i ZOSTAJE
w dzienniku, co mowila zreszta sama tresc komunikatu,
- przywrocenie grafiki domyslnej: plik da sie wgrac z powrotem, a "przywroc
domyslna" to cofniecie, nie zniszczenie.
Przy zatrzymaniu nagrywania tresc powtarzala przy tym ostrzezenie wlasnymi
slowami, wiec to samo bylo napisane dwa razy.
Ostrzezenie uzyte wszedzie przestaje cokolwiek znaczyc tam, gdzie strata jest
prawdziwa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>