Poprzedni commit zmienil typ w rekordzie `Event` na `ModeratorInteractionView`,
a ten sam rekord obsluguje TAKZE temat publiczny: `chatPosted`, `chatRemoved`,
`questionPublished`, `questionUpdated` i `questionRemoved` przekazuja do niego
`InteractionView`. Build backendu przestal sie kompilowac z trzema bledami
"incompatible types". Wdrozenie zatrzymalo sie na tym etapie, wiec produkcja
zostala na poprzednim obrazie i dzialala bez przerwy.
`Event` wraca do widoku publicznego, a temat moderacji dostaje wlasny rekord
`ModeratedInteractionEvent`. Jeden wspolny dla obu tematow byl zreszta zla
konstrukcja niezaleznie od bledu: zmiana typu w jednym miejscu cicho
przepuszczalaby dane moderacji na strumien publiczny albo odwrotnie, a to jest
granica prywatnosci, nie szczegol implementacji.
Nazwa `ModerationEvent` byla juz zajeta przez zdarzenie o wyciszeniu
i zablokowaniu uczestnika, ktore niesie sam identyfikator zapisu. Trzy rozne
zdarzenia maja teraz trzy rozne rekordy.
Blad wyszedl dopiero na wdrozeniu, bo backendu nie da sie u mnie skompilowac:
nie ma Mavena ani Dockera, a proba zlozenia classpath z lokalnego repozytorium
konczy sie kolizja starych bibliotek `javax` z `jakarta` i brakiem Lomboka.
Sprawdzone wiec porownaniem sygnatur: piec wywolan `new Event(...)` odpowiada
pieciu metodom przyjmujacym `InteractionView`, a lancuch moderacji ma
`ModeratorInteractionView` na kazdym etapie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wylaczony TRYB i niepodjeta AKCJA wygladaly identycznie: oba szare. "Otwarty
mikrofon wylaczony" nie roznil sie niczym od "Nagraj spotkanie", chociaz
pierwszy mowi o ograniczeniu nalozonym na uczestnikow, a drugi o czyms, czego
prowadzacy jeszcze nie zrobil.
Teraz kolor niesie znaczenie:
- ZIELONY: tryb wlaczony, uczestnicy moga mowic albo pokazac obraz,
- CZERWONY: tryb wylaczony, uczestnicy sa ograniczeni,
- SZARY: akcja niepodjeta, nic sie nie dzieje.
Nagrywanie ma odwrotna logike i to jest swiadome: czerwien oznacza tam "trwa
zapis", jak dioda REC, a szarosc stan spoczynku. Nie jest ograniczeniem
uczestnikow, wiec czerwien przy wylaczonym sugerowalaby problem tam, gdzie
go nie ma: wiekszosc webinarow nie jest nagrywana i to normalny stan.
Odcien na najechanie to mocniejsza obwodka, a nie `bg-alarm-100`: paleta
HydroNexis ma dla `alarm` wylacznie 50, 500 i 600, wiec posredni odcien
wygenerowalby klase bez pokrycia w tokenach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wyciszenie i blokada byly zbudowane w polowie: backend mial endpointy
`/participants/{id}/mute` i `/ban`, migracja V28 jest, a uczestnik z
wyciszeniem faktycznie nie mogl pisac. Brakowalo jednego ogniwa.
Zdarzenie na temacie moderacji nioslo widok PUBLICZNY, bez identyfikatora
zapisu autora, a panel uzupelnial brakujace pola wartosciami domyslnymi
(`authorRegistrationId: null`). Warunek pokazujacy przyciski sprawdza wlasnie
to pole, wiec:
- wiadomosci wczytane przy wejsciu mialy przyciski,
- wiadomosci przychodzace NA ZYWO mialy tylko kosz.
Komentarz w kodzie zakladal, ze dane uzupelni "najblizsze odswiezenie listy po
akcji mute/ban". Zalozenie bylo bledne: zeby wykonac te akcje, trzeba miec
przycisk. Bez odswiezenia nie ma przycisku, bez przycisku nie ma odswiezenia,
wiec pomagalo dopiero przeladowanie strony. Zgloszone z produkcji.
Temat moderacji niesie teraz PELNY widok moderatora, razem z identyfikatorem
zapisu i stanem wyciszenia autora. Jest to bezpieczne, bo temat subskrybuje
wylacznie panel prowadzacego, uwierzytelniony tokenem administratora.
**Strumien publiczny zostaje bez zmian.** `InteractionView` nadal nie niesie
identyfikatora zapisu ani adresu e-mail: czat widza wszyscy obecni, wiec nic,
co pozwala wskazac konkretna osobe poza spotkaniem, nie moze stamtad wyjsc.
Dlatego doszedl osobny `InteractionService.moderatorView`, a nie pole
dopisane do widoku publicznego.
Mapowanie jest w transakcji: autor to relacja leniwa, a rozglaszanie dzieje sie
juz poza nia.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Interfejs sali mial DWA niezalezne zrodla prawdy o tym, czy uczestnik moze
mowic: napis "Masz glos, mikrofon jest wlaczony" bral sie ze stanu ZGLOSZENIA
(`speak.granted`), a faktyczny mikrofon z uprawnienia w TOKENIE
(`canPublishAudio`). Te dwie rzeczy nie sa tym samym.
Prawo do audio daje takze rola ACTIVE_PARTICIPANT, ktora uczestnik dostaje przy
wlaczonym trybie "wideo dla wszystkich". Odebranie glosu nie zmienia wtedy
uprawnien, ale zmienia stan zgloszenia: napis znikal, a mikrofon zostawal
otwarty. Po kilku cyklach nadania i odebrania obraz w panelu rozjezdzal sie
z tym, co uczestnik faktycznie moze. Zgloszone z produkcji.
Pasek akcji dostaje teraz `canPublishAudio` wprost z tokenu i to ono
rozstrzyga o napisie. Gdy glos jest juz przyznany, a token jeszcze nie dotarl,
pasek mowi "trwa wlaczanie mikrofonu" zamiast klamac w ktorakolwiek strone.
Przycisk "Oddaj glos" zostaje przy zgloszeniu, bo dotyczy wlasnie jego.
SLAD DO DIAGNOZY
Rozjazd miedzy interfejsem a serwerem jest trudny do zbadania po fakcie:
uczestnik zglasza "mikrofon nie dziala", a log serwera pokazuje poprawnie
wydany token. Konsola sali wypisuje teraz po kazdej zmianie stan tokenu obok
stanu zgloszenia i obu trybow, wiec przy nastepnym zgloszeniu widac, ktora
strona jest nieaktualna. To nie jest debugowanie do usuniecia: nigdzie indziej
nie widac obu tych rzeczy naraz.
Hipoteza o wygasajacym tokenie sprawdzona i ODRZUCONA: TTL to szesc godzin,
czyli dluzej niz najdluzszy webinar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kolejka zgloszen i stan glosu polegaly WYLACZNIE na zdarzeniach WebSocket.
Zdarzenie potrafi nie dojsc, a polaczenie nie zawsze to sygnalizuje: karta
uspiona przez system, chwilowa utrata sieci, przelaczenie z WiFi na komorke.
WebSocket nie ma przy tym historii, wiec zgubionego zdarzenia nikt nie powtorzy.
Prowadzacy widzial przez to "Nikt nie prosi o glos" przy zgloszeniach
czekajacych juz kilka minut i dowiadywal sie o nich dopiero po odswiezeniu
strony.
Po stronie UCZESTNIKA bylo grozniej: stan pobieral sie RAZ, przy wejsciu na
sale. Zgubione zdarzenie o przyznaniu glosu oznaczalo, ze uczestnik nie
dowiadywal sie o nim wcale: przycisk zostawal w starym stanie, a mikrofon
zamkniety, chociaz uprawnienie po stronie serwera juz bylo. Wyglada to na
niedzialajacy mikrofon, a nie na zgubione zdarzenie, wiec szukanie przyczyny
idzie w zla strone.
Odpytywanie jest DRUGA droga, nie glowna: WebSocket zostaje, bo prowadzacy ma
widziec podniesiona reke od razu. Panel odswieza kolejke co 5 sekund,
uczestnik swoj stan co 10. Odpowiedz to kilka wierszy albo pojedynczy obiekt,
wiec koszt jest znikomy nawet przy duzej sali. Ten sam wzorzec co przy banerach,
gdzie front takze nie polega wylacznie na zdarzeniach.
Wczesniejsza poprawka (dociagniecie stanu w `onConnect`) lapala tylko jeden
przypadek: ponowne polaczenie. Nie pomagala, gdy polaczenie formalnie zyje,
a ginie pojedyncze zdarzenie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
SKRYPT WDROZENIA
`deploy-prod.sh` istnial wylacznie na serwerze, tak samo jak wczesniej cztery
zmienne LiveKit w compose: kto odtwarzalby instancje z repozytorium, nie
dostalby go wcale. Wchodzi do `scripts/` w wersji dzialajacej, z jedna zmiana:
sprawdzeniem czystosci repozytorium na starcie.
Powod jest praktyczny. `git merge --ff-only` odrzuca wdrozenie, gdy na serwerze
sa lokalne zmiany, ale robi to CICHO: deploy konczy sie statusem "sukces",
a kod nie wchodzi na serwer. Zdarzylo sie to dzis i wykrylo dopiero reczne
`git log` na produkcji. Teraz skrypt przerywa i wypisuje pliki.
NAZWA PROJEKTU W KOMENDACH COMPOSE
README uczyl komend produkcyjnych BEZ `-p`, czyli dokladnie tych, ktore
wchodza w cudzy projekt. Katalog aplikacji nazywa sie `app`, wiec compose
bierze z niego nazwe projektu, a na serwerze z kilkoma aplikacjami trafia
w nie te. Zdarzylo sie realnie: `up -d --force-recreate coturn` utworzylo
kontener w projekcie drugiej aplikacji tego samego wlasciciela, walczacy
o ten sam port 3478. Jedynym ostrzezeniem byl komunikat "Found orphan
containers", latwy do przeoczenia.
Poprawione 11 komend produkcyjnych plus dwie nowe sekcje: o nazwie projektu
i o tym, ze `docker restart` NIE przeladowuje zmiennych z `.env` (argumenty
sa zamrozone przy tworzeniu kontenera, wiec po zmianie hasla kontener wstaje
ze stara wartoscia, wygladajac na dzialajacy poprawnie). Komendy lokalne
zostaja bez `-p`: tam nazwa z katalogu nikomu nie wchodzi w droge.
INSTRUKCJA TWORZENIA WEBINARU
Uzupelniona o funkcje dodane w ostatnich dniach: link do sali trafiajacy do
uczestnika automatycznie (potwierdzenie, przypomnienia, strona zgloszenia),
nagrywanie wlaczane przyciskiem w panelu prowadzenia oraz rozmycie tla.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Backend nie dostawal ani adresu serwera LiveKit, ani kluczy API: brakowalo
czterech linii w sekcji `environment`. Bez nich WIDEO NIE DZIALA WCALE, wiec
na dzialajacej instancji zmiana byla dopisana recznie i istniala WYLACZNIE
w pliku roboczym na serwerze. `git checkout` albo konflikt przy `pull` skasowalby
ja bez sladu, a kto odtwarzalby instancje z repozytorium, nie dowiedzialby sie
o niej wcale.
Komentarze w obu plikach mowily przy tym cos innego niz stan faktyczny:
compose sugerowal, ze konfiguracja wideo idzie przez panel jak SMTP,
a `.env.production.example` twierdzil wprost, ze "klucze API ustawia sie
w panelu". Stad brala sie mylaca obserwacja, ze `app_settings` nie ma zadnego
klucza LIVEKIT mimo dzialajacego wideo: to jest stan NORMALNY, bo
`SettingsService` czyta baze, potem srodowisko, potem wartosc domyslna.
Wideo idzie przez srodowisko swiadomie: dostawca i klucze naleza do
infrastruktury instancji, tak samo jak adres MinIO czy baza, a adres LiveKit
musi zgadzac sie z domena obslugiwana przez nginx hosta. Ustawia je wiec ten,
kto wdraza, a nie administrator tresci.
Dopisane takze `LIVEKIT_INTERNAL_URL`, ktorego w pliku wzorcowym nie bylo,
choc `SettingKey` zna je od poczatku: adres widziany przez backend bywa inny
niz ten dla przegladarki, a bez niego nagrywanie przez Egress probuje laczyc
sie z serwerem droga publiczna.
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>
`ReminderScheduler` pisal "Przypomnienia pojda bez linku do spotkania" przy
kazdym webinarze z pustym `join_url`, czyli przy KAZDYM webinarze na LiveKit
i Jitsi: pokoj jest tam wewnetrzny, a to pole zostaje puste z zalozenia.
Po dodaniu linku `/spotkanie/{token}` ostrzezenie mowilo administratorowi
NIEPRAWDE, bo link jest w kazdej wiadomosci.
Brak adresu boli naprawde tylko w trybie NONE: tam obraz jest w cudzym
narzedziu, a `join_url` wklejony przez organizatora jest jedyna droga na
spotkanie. Rozstrzyga o tym `supportsRooms()`, czyli to samo zrodlo, ktorego
uzywa `meetingUrlFor` przy skladaniu linku: dwie kopie tej reguly rozjechalyby
sie przy pierwszej poprawce.
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>
Uczestnik przy dostawcy z wlasnymi pokojami (LiveKit, Jitsi, Daily, Zoom)
nie mial ZADNEJ drogi na sale:
- mail potwierdzajacy prowadzil na /w/{slug}, czyli formularz zapisu,
- strona zgloszenia renderowala przycisk "Dolacz do spotkania" wylacznie
gdy `webinars.join_url` nie bylo puste, a przy LiveKit pokoj jest
wewnetrzny i to pole zostaje NULL,
- mail obiecywal przy tym wprost "link do spotkania znajdziesz na stronie
swojego zgloszenia".
Adres sklada teraz `VideoProviderFactory.meetingUrlFor`, bo tylko backend
zna token zapisu. Tryb NONE oddaje `join_url` wklejony przez organizatora,
kazdy inny dostawca wlasna trase /spotkanie/{token}. Ta sama wartosc zasila
widok zgloszenia i szablony maili, wiec nie ma dwoch miejsc do poprawiania.
Szablon potwierdzenia dostaje przycisk wejscia jako pierwszy, nad odnosnikiem
do strony webinaru, oraz zdanie o tym, ze sala otwiera sie przed startem:
bez niego uczestnik klikajacy tydzien wczesniej widzi odliczanie i nie wie,
czy tak ma byc. Szablon przypomnienia mial obsluge `joinUrl` od poczatku,
wiec naprawia sie sam.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>