Commit Graph

14 Commits

Author SHA1 Message Date
debian 4f21af5f80 Grafika strony glownej z biblioteki mediow, naglowek w panelu
Deploy PROD / deploy (push) Successful in 26s
GRAFIKA JAKO USTAWIENIE, NIE PLIK W REPOZYTORIUM
Nowe `ACADEMY_HERO_IMAGE_MEDIA_ID` (typ MEDIA) wskazuje zdjecie obok naglowka
strony glownej. Zdjecie jest materialem konkretnego klienta, zwykle z jego
logo, a model wdrozenia zabrania danych klienta w obrazie Docker: wbudowane
w kod pojawiloby sie na stronie kazdej kolejnej instancji. Ta sama regula co
przy logo i ikonie karty.

Plik idzie przez `/api/public/media/{id}`, jak obrazki w tresci strony. Pusta
albo nieliczbowa wartosc daje domyslny panel z motywem wodnym zamiast adresu
donikad: zepsuty obrazek w najbardziej widocznym miejscu strony wyglada gorzej
niz motyw domyslny.

Ramka dostaje proporcje zdjecia (16:9) zamiast sztywnej wysokosci: przy innym
ksztalcie `object-cover` obcinal brzegi, a tam zwykle stoi logo.

NAGLOWEK STRONY GLOWNEJ W PANELU
`ACADEMY_HERO_EYEBROW`, `_TITLE` i `_TEXT` istnialy w backendzie od poczatku,
ale nie bylo ich w panelu: tytul strony dalo sie zmienic wylacznie zapytaniem
do bazy. Ustawienia -> Strona glowna ma teraz karte z tymi polami i grafika.

ODMIANA LICZNIKOW
Etykiety licznikow mialy na sztywno forme dla "wielu", wiec na zywej stronie
widnialo "1 kursantow". Licznik jest pierwsza rzecza, ktora widzi gosc.
Uzyta istniejaca funkcja `plural` z `lib/dates.ts`, teraz wyeksportowana,
zamiast czwartej kopii tej samej funkcji.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:43:55 +02:00
debian 305b1e6144 Usuwanie kontaktow, blokowanie i usuwanie kont
Deploy PROD / deploy (push) Successful in 26s
KONTAKTY
Usuwanie istnialo w API, ale nie mialo przycisku na liscie. Doszedl, wraz
z endpointem `GET /admin/contacts/{id}/deletion-impact`: panel pyta
o potwierdzenie DOPIERO Z LICZBAMI, tak samo jak przy usuwaniu webinaru.

Jest to konieczne, bo usuniecie kontaktu kasuje KASKADOWO zapisy na kursy
i zgody. Przy zwyklym wpisie marketingowym odpowiedz jest zupelnie inna niz
przy kursancie z oplaconym kursem, ktoremu usuniecie odbiera dostep do
materialow. Pytanie mowi to wprost i dodaje, ze zamowienia zostaja: sa
dokumentem finansowym i maja `ON DELETE SET NULL`.

Gdy pobranie skutkow sie nie uda, potwierdzenie NIE JEST pokazywane wcale.
Pytanie o zgode na cos, czego skutkow nie udalo sie ustalic, byloby gorsze
niz brak pytania.

UZYTKOWNICY
Byla sama dezaktywacja. Doszly:

- odblokowanie (`POST /{id}/activate`), bo dezaktywacja jest odwracalna
  z zalozenia: konto zablokowane na czas urlopu ma dac sie wlaczyc bez
  zakladania nowego i bez tracenia sladu w dzienniku,
- usuniecie trwale (`DELETE /{id}`) dla kont zalozonych przez pomylke.

APLIKACJA NIE MOZE ZOSTAC BEZ ADMINISTRATORA
Usuwanie ma te same dwie bariery co dezaktywacja i uzywa TEJ SAMEJ metody
`assertNotLastActiveAdmin`, a nie drugiej kopii reguly: nie mozna usunac
wlasnego konta ani ostatniego aktywnego administratora. Jedno i drugie
konczyloby sie utrata dostepu do panelu bez mozliwosci odzyskania go
z poziomu aplikacji.

PRACA UZYTKOWNIKA NIE PRZEPADA
Wszystkie trzy odwolania do `users` maja `ON DELETE SET NULL`: pliki
w bibliotece, ogloszone zadania i odpowiedzi na pytania zostaja, tracac
jedynie informacje o autorze. Kasowanie kaskadowe zabieraloby tresc szkolen
razem z kontem, ktore ja wprowadzilo.

DEZAKTYWACJA NIE JEST OPERACJA NISZCZACA
Jej potwierdzenie celowo NIE ma czerwonego ostrzezenia: tresc mowi wprost, ze
konto da sie wlaczyc z powrotem, a od teraz jednym klknieciem obok. Czerwien
przeczylaby wlasnemu komunikatowi i rozmydlala sygnal tam, gdzie strata jest
prawdziwa, czyli przy usuwaniu konta i kontaktu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 08:40:16 +02:00
debian 0292b03d5e Zadanie ubite razem z kontenerem znika z ekranu Zasoby po osmiu godzinach
Deploy PROD / deploy (push) Successful in 19s
Egress zapisuje stan zadan w Redisie, a status koncowy ustawia sam proces
obslugujacy nagranie. Gdy proces zostanie ubity razem z kontenerem (restart,
wyczerpanie pamieci), status nigdy sie nie zmienia: zadanie figuruje jako
EGRESS_ACTIVE **na zawsze**.

`StopEgress` konczy sie wtedy sukcesem i NIC NIE ZMIENIA, bo nie ma juz czego
zatrzymywac. Sprawdzone na zywo: wpis wisial po restarcie Egress, panel
pokazywal go jako osierocone zadanie, a klikniecie "Zatrzymaj" logowalo
"Zatrzymano Egress" i zostawialo wszystko po staremu. Zuzycie procesora bylo
przy tym zerowe, czyli zadanie faktycznie nie istnialo.

Ekran mowil wiec nieprawde i nie dawalo sie tego odkrecic z poziomu aplikacji:
wpis usunieto recznie z Redisa.

Liczy sie teraz takze WIEK zadania, nie tylko jego status. Osiem godzin: dluzej
niz najdluzszy sensowny webinar razem z montazem, a krocej niz doba, zeby wpis
po nocnym restarcie nie straszyl rano.

`startedAt` jest w nanosekundach od epoki. Proto podaje sam `int64` bez
jednostki, ale webhooki LiveKit niosa tam wartosci 19-cyfrowe, a `duration`
w `FileInfo` jest liczone tak samo. Zero oznacza zadanie, ktore nie zdazylo
wystartowac: traktujemy je jak biezace, zeby swiezo zlecone nagranie nie
znikalo z ekranu, zanim ruszy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 08:19:12 +02:00
debian 4203ca036e Usuwanie nagrania i webinaru zatrzymuje zadanie u dostawcy
Deploy PROD / deploy (push) Successful in 19s
Ekran Zasoby pozwalal POSPRZATAC osierocone nagrania, ale nie zapobiegal ich
powstawaniu. Dwie sciezki tworzyly je dalej:

- `RecordingService.delete` kasowal wpis, zostawiajac zadanie dzialajace
  u dostawcy w nieskonczonosc,
- `WebinarService.delete` kasowal nagrania KASKADOWO, przez co trwajace
  zadanie stawalo sie niewidoczne dla aplikacji: scheduler domykajacy
  nagrania szuka po naszych wpisach, a tych juz nie bylo.

Ta druga sciezka wywolala realny incydent: webinar usunieto w trakcie
nagrywania, a przegladarka po stronie Egress krecila sie kilkanascie godzin
i zjadala jedna piata procesora na serwerze dzielonym z dwiema innymi
aplikacjami. Zaden ekran tego nie pokazywal.

Obie zatrzymuja teraz zadanie PRZED skasowaniem wpisu.

BLAD ZATRZYMANIA NIE BLOKUJE USUNIECIA
Najczestsza przyczyna to zadanie, ktore samo sie juz zakonczylo, a administrator
ma prawo posprzatac wpis niezaleznie od tego, co odpowie dostawca. Slad idzie
do logu, a zadanie mimo wszystko dzialajace pokaze ekran Zasoby jako osierocone,
razem z przyciskiem zatrzymania. Zabezpieczenie dziala wiec z dwoch stron:
zapobiega i naprawia.

Sprawdzone, ze nie powstaje cykl zaleznosci: `LiveKitEgressService` zalezy
wylacznie od repozytoriow, ustawien i fabryki dostawcow, wiec nie siega
z powrotem ani do `RecordingService`, ani do `WebinarService`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 08:11:25 +02:00
debian f35bfe1b8c Sekcja Zasoby: podglad i sprzatanie zadan w tle
Deploy PROD / deploy (push) Successful in 26s
Kilka rzeczy w tej platformie zyje dluzej niz zadanie HTTP, ktore je
uruchomilo: nagranie u dostawcy, wiadomosc w kolejce, podejscie do quizu,
otwarta sesja obecnosci. Kazda moze utknac, a administrator nie mial jak sie
o tym dowiedziec: w panelu widac skutek (brak nagrania, niewyslany mail),
a nie przyczyne.

POWSTALO PO REALNYM ZDARZENIU
Nagranie zostalo osierocone, bo webinar usunieto w trakcie: wpis znikl
kaskadowo z bazy, a zadanie u dostawcy nagrywalo dalej. Przegladarka Chrome
po stronie Egress krecila sie KILKANASCIE GODZIN i zjadala jedna piata
procesora na serwerze dzielonym z dwiema innymi aplikacjami. Zaden ekran tego
nie pokazywal; wyszlo przypadkiem, przy sprawdzaniu czegos innego.

Zadanie osierocone jest przy tym nie do zatrzymania zwyklymi srodkami:
scheduler domykajacy nagrania szuka po NASZYCH wpisach, wiec czegos, czego
w bazie nie ma, nigdy nie zobaczy.

CO POKAZUJE
Sekcje w kolejnosci waznosci: zadania osierocone (dzialaja u dostawcy, brak
wpisu w bazie), nagrania w toku z oznaczeniem wiszacych po szesciu godzinach,
kolejka wiadomosci QUEUED i FAILED wraz z trescia bledu, liczniki porzuconych
podejsc do quizow i nieswiezych sesji obecnosci. Kazda pozycja ma akcje:
zatrzymanie, usuniecie wpisu, ponowienie wysylki, reczne domkniecie.

Zatrzymanie nagrania idzie po identyfikatorze U DOSTAWCY, a nie naszym: to
jedyna droga do czegos, co po naszej stronie juz nie istnieje. Usuniecie wpisu
najpierw probuje zatrzymac zadanie, dopiero potem kasuje: odwrotna kolejnosc
zostawialaby dzialajace nagranie bez zadnego sladu, czyli dokladnie ten stan,
ktoremu ten ekran ma zapobiegac.

BLAD JEDNEJ SEKCJI NIE PRZEWRACA CALOSCI
Odpytanie dostawcy idzie po sieci i bywa niedostepne. Wtedy pokazujemy
ostrzezenie i informacje, ze lista osieroconych moze byc niepelna, a reszta
ekranu dziala. Ekran diagnostyczny, ktory sam sie nie otwiera, jest
bezuzyteczny dokladnie wtedy, gdy jest potrzebny.

CZEGO TU NIE MA I NIE BEDZIE
Procesow systemowych ani kontenerow Dockera. Backend nie ma dostepu do gniazda
Dockera i to jest bariera bezpieczenstwa, nie przeoczenie: kto przejmie panel,
przejalby caly serwer razem z aplikacjami stojacymi obok. Do zarzadzania
kontenerami sluzy SSH. Ekran pokazuje wylacznie to, co aplikacja wie o sobie.

Dostep wylacznie dla ADMIN, regula w `SecurityConfig` przed ogolna `/admin/**`:
zatrzymanie zadania dotyczy calej instancji, takze cudzych webinarow, a
moderator prowadzi spotkania, nie zarzadza infrastruktura.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 08:06:19 +02:00
debian ecece16352 Osobny rekord zdarzenia dla tematu moderacji
Deploy PROD / deploy (push) Successful in 26s
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>
2026-09-04 15:31:38 +02:00
debian 6ea59e7f5e Moderacja uczestnika dziala takze przy wiadomosciach na zywo
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>
2026-09-04 15:26:29 +02:00
debian 0421f19630 Nagrywanie spotkania: przycisk w panelu i domykanie statusu
Deploy PROD / deploy (push) Successful in 26s
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>
2026-09-04 10:55:39 +02:00
debian fc1726b8b7 Ostrzezenie o braku join_url dotyczy juz tylko trybu NONE
`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>
2026-09-04 10:55:14 +02:00
debian 2bc2e6525c Prelegent widzi zadania i banery na sali
`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>
2026-09-04 09:44:28 +02:00
debian 1dadc6f6c4 Link do sali w mailu i na stronie zgloszenia
Deploy PROD / deploy (push) Successful in 20s
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>
2026-09-04 07:53:09 +02:00
debian 9bd59d60bb API + kosmetyka
Deploy PROD / deploy (push) Successful in 25s
2026-09-02 11:55:59 +02:00
debian 7b4d367a06 regulamin i polityka na stronie głównej
Deploy PROD / deploy (push) Successful in 25s
2026-09-02 10:56:41 +02:00
debian 84eec0039e Initial commit 2026-09-02 08:21:27 +02:00