# Architektura SameBeat

Ten dokument opisuje, jak działa SameBeat "od środka" -- bez kodu, na poziomie
"co gada z czym i po co". Jeśli jesteś praktykiem, nie programistą -- to jest
ten plik, od którego warto zacząć.

## Co to w ogóle jest

SameBeat to internetowa impreza na żywo, w przeglądarce. Działa jak połączenie
radia (leci muzyka, wszyscy słyszą to samo w tym samym momencie) i reżyserowanej
sceny wideo (host, czyli "reżyser imprezy", w danej chwili oddaje mikrofon i
kamerę jednej osobie na krótki czas, np. 5 sekund, żeby mogła coś powiedzieć/
pokazać wszystkim uczestnikom).

Dwa rodzaje ruchu w systemie, i to jest kluczowe do zrozumienia całej reszty:

1. **Dźwięk i obraz** (audio/wideo z mikrofonu i kamery) -- to leci osobnym,
   wyspecjalizowanym torem, bo wymaga bardzo małych opóźnień.
2. **Sterowanie i zdarzenia** (kto teraz ma mikrofon, kto dołączył, czat,
   reakcje, zmiana utworu) -- to leci drugim torem, jak SMS-y w tle imprezy.

Rozdzielenie tych dwóch torów to najważniejsza decyzja architektoniczna
w całym systemie.

## Z czego się składa (klocki systemu)

| Klocek | Co robi | Odpowiednik "z życia" |
|---|---|---|
| **Frontend** (React + TypeScript, budowany narzędziem Vite) | To, co widzi uczestnik w przeglądarce: scena, lista uczestników, czat, panel hosta | Ekran telewizora + pilot |
| **Backend** (ASP.NET Core 8) | Mózg systemu: kto jest zalogowany, czyja jest kolej na mikrofon, historia imprezy, reguły gry | Reżyser + scenariusz |
| **PostgreSQL** | Baza danych -- trwałe zapisywanie kont, imprez, historii | Segregator z dokumentami imprezy |
| **SignalR** | Kanał sterowania w czasie rzeczywistym (część ASP.NET Core) -- backend "woła" do wszystkich przeglądarek naraz, gdy coś się zmienia | Interkom / megafon reżysera |
| **LiveKit** (self-hosted, czyli postawiony na własnym serwerze, nie w chmurze firmy trzeciej) | Przesyła dźwięk i obraz między uczestnikami z minimalnym opóźnieniem | Mikser dźwięku + kamery na scenie |
| **Nginx** | "Portier" na wejściu do serwera -- przyjmuje ruch z internetu i kieruje go do backendu/frontendu/LiveKit, obsługuje szyfrowanie (HTTPS) | Recepcja budynku, kieruje gości do właściwych drzwi |
| **TURN/Coturn** | Pomaga połączyć dwie przeglądarki, które siedzą za trudnymi routerami/NAT-em (typowe domowe łącza, sieci firmowe) | Pośrednik, gdy dwie osoby nie mogą się dogadać bezpośrednio |

### Słowniczek skrótów użytych wyżej

- **NAT** -- mechanizm w routerach domowych/firmowych, który "chowa" wiele
  urządzeń za jednym adresem internetowym. Utrudnia to bezpośrednie
  połączenia między dwiema przeglądarkami.
- **SDK/API** -- w tym dokumencie nieistotne, pomijamy.
- **Self-hosted** -- postawione i utrzymywane na własnym serwerze, a nie
  wynajęte jako gotowa usługa od firmy trzeciej.

## Dwa przepływy danych -- to jest sedno architektury

### Przepływ 1: Dźwięk/obraz (uczestnik -> LiveKit -> widzowie)

```
 [Mikrofon/kamera        [Serwer LiveKit]         [Przeglądarki
  uczestnika z            (odbiera strumień          wszystkich
  mikrofonem]              i rozsyła dalej)          widzów]

  Uczestnik A  ────audio/wideo───▶  LiveKit  ───audio/wideo───▶  Widz 1
  (ma właśnie                                 ───audio/wideo───▶  Widz 2
   głos)                                      ───audio/wideo───▶  Widz 3
                                               ───audio/wideo───▶  ... Widz N
```

- Uczestnik, który dostał głos od hosta, wysyła swój dźwięk/obraz do
  **LiveKit**.
- LiveKit rozsyła ten strumień do wszystkich pozostałych przeglądarek
  (widzów) -- każdy widzi i słyszy to samo, prawie bez opóźnienia.
- Ten tor **nie przechodzi przez backend** -- backend nie ma z tym nic
  wspólnego, poza tym że wcześniej wydał uczestnikowi "przepustkę"
  (token) mówiącą "możesz teraz nadawać".
- Połączenie między przeglądarką a LiveKit czasem wymaga pomocy TURN/Coturn
  (gdy bezpośrednie połączenie się nie udaje przez NAT).

### Przepływ 2: Sterowanie (host -> SignalR -> wszyscy)

```
 [Panel hosta]         [Backend ASP.NET Core]        [Przeglądarki
  (reżyser klika          + SignalR                   wszystkich
  "daj mikrofon           (interkom)                  uczestników]
  Uczestnikowi A")

  Host  ──"daj głos A"──▶  Backend  ──zdarzenie──▶  Uczestnik A (dostaje
                              │                       przepustkę do LiveKit)
                              ├──zdarzenie──▶  Widz 1 (scena się
                              │                 przełącza na A)
                              ├──zdarzenie──▶  Widz 2 (to samo)
                              └──zapis do───▶  PostgreSQL
                                 bazy          (historia: kto, kiedy,
                                                jak długo miał głos)
```

- Host klika w panelu "teraz mówi uczestnik A, przez 5 sekund".
- Backend to przyjmuje, zapisuje decyzję (np. do PostgreSQL, jeśli ma
  zostać w historii imprezy) i **rozsyła zdarzenie przez SignalR** do
  wszystkich podłączonych przeglądarek naraz.
- Każda przeglądarka dostaje ten sam sygnał w tym samym momencie i
  aktualizuje ekran: "teraz na scenie jest A".
- Backend osobno wydaje uczestnikowi A jednorazową przepustkę (token) do
  LiveKit, żeby mógł zacząć nadawać dźwięk/obraz -- to łączy oba przepływy.
- Po upływie czasu (np. 5 s) backend automatycznie odbiera mikrofon i
  wysyła kolejne zdarzenie: "koniec, ktoś inny może dostać głos".

## Całość na jednym obrazku

```
                         INTERNET
                            │
                      ┌─────▼─────┐
                      │   Nginx   │   (wejście, HTTPS, kierowanie ruchu)
                      └──┬─────┬──┘
                         │     │
            ┌────────────┘     └────────────┐
            │                                │
     ┌──────▼──────┐                  ┌──────▼──────┐
     │  Frontend    │                  │   Backend    │
     │  (pliki      │◀──HTTP/SignalR──▶│  ASP.NET     │
     │  statyczne   │   (sterowanie)   │  Core 8      │
     │  w przegl.)  │                  └──┬────────┬──┘
     └──────┬───────┘                     │        │
            │                       ┌─────▼──┐  ┌──▼─────────┐
            │  audio/wideo          │Postgre  │  │  LiveKit    │
            └───────────────────────▶  SQL    │  │ (dźwięk/    │
                     (bezpośrednio  └─────────┘  │  wideo)     │
                      do LiveKit,                └──────┬──────┘
                      nie przez backend)                │
                                                   ┌─────▼──────┐
                                                   │TURN/Coturn  │
                                                   │(pomoc przy  │
                                                   │ trudnym NAT)│
                                                   └─────────────┘
```

## Dlaczego tak, a nie inaczej (krótkie uzasadnienie)

- **Dźwięk/obraz osobno od sterowania** -- gdyby audio/wideo szło przez
  backend i SignalR, opóźnienia byłyby zbyt duże (SignalR jest świetny do
  "kto ma głos", fatalny do przesyłania samego dźwięku na żywo do wielu
  osób). LiveKit jest do tego zbudowany specjalnie.
- **Self-hosted LiveKit** -- pełna kontrola nad kosztami i danymi, brak
  zależności od zewnętrznej firmy przy każdej imprezie.
- **SignalR zamiast odpytywania co sekundę** -- backend "puka" do
  przeglądarek, gdy coś się dzieje, zamiast każda przeglądarka pytała co
  chwilę "czy coś się zmieniło?". Mniej obciążenia serwera, szybsza reakcja.
- **PostgreSQL** -- klasyczna, sprawdzona baza danych do rzeczy, które muszą
  przetrwać restart serwera (konta, historia imprez).

## ETAP 2 -- dołączanie i poczekalnia

Zanim ktoś trafi na scenę imprezy, musi przejść przez `/join`. To jest
"drzwi wejściowe" -- oddzielone od reszty systemu, żeby host miał kontrolę
nad tym, kto w ogóle się pojawia.

Flow krok po kroku:

1. Uczestnik wchodzi na `/join` (link do konkretnej imprezy).
2. Podaje nick (bez rejestracji/hasła -- to jest gość, nie konto).
3. Przeglądarka pyta o zgodę na kamerę (uczestnik musi kliknąć "zezwól").
4. Frontend woła `POST /api/join`. Backend:
   - tworzy wpis w **poczekalni** (lista osób czekających na wpuszczenie),
   - wydaje **token LiveKit z prawem pokazania kamery -- ale tylko kamery,
     nigdy mikrofonu, i tylko w osobnym "pokoju poczekalni"** (patrz niżej,
     dlaczego kamera działa już na tym etapie, a nie dopiero po akceptacji).
5. Host w panelu widzi listę oczekujących w poczekalni razem z ich kamerami
   (właśnie po to trzeba było wydać token z prawem pokazania obrazu -- host
   musi zobaczyć, kto czeka, zanim podejmie decyzję) i dla każdej osoby ma
   trzy przyciski: **podejrzyj**, **odrzuć**, **zbanuj**.
6. Wpuszczenie na właściwą scenę imprezy (widoczną dla wszystkich widzów) to
   osobna decyzja hosta, która dojdzie w **ETAPIE 3** -- na razie poczekalnia
   kończy się na podglądzie/odrzuceniu/zbanowaniu, slotów sceny jeszcze nie ma.

### Kluczowa zmiana bezpieczeństwa (po prostu, o co chodzi)

Wyobraź sobie token LiveKit jak plastikową bransoletkę na wejściu na
imprezę -- pokazujesz ją i system wie, co ci wolno. Tu chodzi o **dwie
osobne rzeczy**, które łatwo pomylić: co komu wolno (uprawnienia) i gdzie
kto w ogóle jest wpuszczony (pokój).

**1. Uprawnienia -- zawsze decyduje serwer, nigdy przeglądarka**

- **Bransoletka widza** (tylko oglądanie/słuchanie) -- to jedyna
  bransoletka, jaką dostaje każdy, kto woła publiczny, niezalogowany
  endpoint tokenu. Nie da się nią nic nadawać, tylko odbierać.
- **Bransoletka "mogę pokazać kamerę, ale nie mikrofon"** -- tę wydaje
  **wyłącznie** `POST /api/join`, z tożsamością generowaną przez serwer
  (nie przez klienta), i fizycznie ogranicza źródła publikacji do samej
  kamery -- LiveKit odrzuci próbę wysłania dźwięku, nawet gdyby ktoś
  próbował to zrobić z poziomu przeglądarki.
- Wcześniej (przed tą zmianą) istniało ryzyko, że ktoś spreparuje żądanie i
  poprosi o token z wyższymi uprawnieniami, niż powinien mieć. Teraz to
  niemożliwe -- backend wydaje tokeny z góry ustalonym zakresem praw, klient
  nie ma jak tego "podbić".

**2. Pokoje -- poczekalnia i scena główna to dwa OSOBNE pokoje LiveKit**

Samo ograniczenie uprawnień nie wystarczy, jeśli wszyscy (widz, uczestnik,
host) łączą się do jednego wspólnego pokoju -- wtedy każdy anonimowy widz
mógłby podejrzeć kamerę osoby czekającej w poczekalni, zanim host zdąży ją
w ogóle zobaczyć. Dlatego:

- **Pokój "poczekalnia"** -- tu trafiają uczestnicy zaraz po `/join` i tu
  host ich podgląda. Widz nigdy nie dostaje przepustki do tego pokoju.
- **Pokój "scena główna"** -- tu (od ETAPU 3) trafi to, co widzą wszyscy
  widzowie. Uczestnik oczekujący nie jest tu widoczny, dopóki host go nie
  wpuści.

To domyka lukę: sama obecność w poczekalni nie oznacza już, że ktokolwiek
poza hostem może to zobaczyć -- widoczność i prawo do nadawania to zawsze
osobna, świadoma decyzja backendu (czyli w praktyce: hosta).

## ETAP 3 -- scena i sloty

To jest ciąg dalszy poczekalni z ETAPU 2: host bierze kogoś z listy
oczekujących i stawia na scenę, którą widzą już wszyscy widzowie.

Flow krok po kroku:

1. Host w panelu przeciąga uczestnika z poczekalni na jeden ze slotów sceny
   (albo klika odpowiedni przycisk przy danym slocie) -- frontend hosta woła
   `POST /api/host/stage/slots/{numer}/assign`.
2. Backend oznacza tego uczestnika w bazie jako "na scenie" w danym slocie --
   ale **nie wysyła mu przez SignalR żadnego tokenu**. SignalR to kanał
   nadawany do wszystkich naraz, łącznie z anonimowymi widzami, a token do
   sceny głównej to bilet uprawniający do nadawania kamery/mikrofonu -- taki
   bilet nie może polecieć w eterze, gdzie każdy by go podsłuchał.
3. Przeglądarka uczestnika sama, prywatnie, dopytuje backend "czy już mnie
   wpuszczono na scenę?" -- używając sekretu, który dostała tylko ona przy
   dołączeniu (`POST /api/join` z ETAPU 2). Dopiero na tę prywatną odpowiedź
   backend wydaje jej nowy token do pokoju "scena główna", widocznego dla
   wszystkich.
4. Widzowie i host dowiadują się o zmianie przez SignalR -- ale tylko
   informacji "kto jest w którym slocie" (nick + numer slotu), nigdy niczego,
   co dawałoby prawo do publikowania czyjejś kamery czy mikrofonu.

Innymi słowy: to jak ochrona na koncercie, która wpuszcza kogoś na scenę
przez osobne, prywatne drzwi za kulisami -- nie przez główną widownię, gdzie
każdy by to zobaczył.

### Notatka: zdejmowanie ze sceny na razie jest "miękkie"

Gdy host zdejmuje kogoś ze sceny, na razie działa to tak, że przeglądarka
uczestnika sama przestaje nadawać, gdy dostanie o tym informację (przez
SignalR). To wystarcza w normalnych warunkach, ale nie jest twardym
zabezpieczeniem -- ktoś ze zmodyfikowaną przeglądarką teoretycznie mógłby
zignorować to polecenie i dalej nadawać, dopóki jego token nie wygaśnie.
Twarde wymuszenie po stronie serwera LiveKit (czyli fizyczne odcięcie
nadawania, niezależnie od tego, co robi przeglądarka uczestnika) dojdzie w
**ETAPIE 7**, razem z resztą zabezpieczeń.

## ETAP 4 -- muzyka i synchronizacja

To jest trzeci tor danych w systemie, obok audio/wideo (LiveKit) i sterowania
(SignalR) -- ale działa zupełnie inaczej niż oba poprzednie.

**Muzyka NIE leci przez LiveKit ani przez SignalR.** Żaden dźwięk utworu nie
jest strumieniowany z jednej przeglądarki do drugiej. Zamiast tego:

- Każda przeglądarka -- i hosta, i każdego widza -- **sama odtwarza ten sam
  plik audio** pod tym samym adresem URL (tak jak zwykły `<audio>` w
  przeglądarce, każdy u siebie).
- Backend nie przesyła dźwięku. Backend trzyma tylko **"zegar"**: informację,
  o której godzinie utwór zaczął grać i od którego miejsca w utworze (np.
  "ten kawałek wystartował o 14:32:07, od 12. sekundy").
- Ten zegar backend rozsyła przez SignalR (ten sam kanał sterowania co reszta
  zdarzeń) -- ale rozsyła tylko liczby ("od kiedy", "od którego miejsca"),
  nigdy samego dźwięku.

Odpowiednik z życia: to nie jest jak wspólne słuchanie przez telefon (gdzie
jedna osoba "puszcza" drugiej dźwięk), tylko jak umówienie się "wszyscy macie
tę samą płytę u siebie, startujemy o 14:32:07 od 12. sekundy" -- każdy
odpala własny odtwarzacz.

### Jak przeglądarki utrzymują zgranie w czasie

Zegary komputerów/telefonów lekko "pływają" -- po kilku minutach odtwarzanie
w różnych przeglądarkach zacznie się rozjeżdżać, nawet jeśli wszystkie
wystartowały w tym samym momencie. Dlatego:

- Co kilka sekund każda przeglądarka sama sobie liczy: "sądząc po zegarze
  serwera, w którym miejscu utworu powinienem teraz być?".
- Jeśli jest drobna różnica względem tego, co faktycznie gra -- przeglądarka
  **delikatnie koryguje tempo odtwarzania** (troszkę przyspiesza albo
  zwalnia), zamiast robić skok/przeskok w utworze.
- Dzięki temu nie słychać "szarpnięć" przy drobnych rozjazdach, a wszyscy
  słuchają "prawie" w tym samym momencie. Twardy przeskok (skok w miejscu
  odtwarzania) zostaje w zapasie tylko na duże rozjazdy, których delikatna
  korekta tempa by nie nadgoniła.

### Notatka: auto-przejście do następnego utworu -- na razie zależne od hosta

Automatyczne przejście do kolejnego utworu na koniec listy działa **na razie
tylko wtedy, gdy panel hosta jest otwarty w przeglądarce** -- to przeglądarka
hosta wykrywa koniec odtwarzania i "woła" do backendu "graj następny". Jeśli
host zamknie panel (albo straci połączenie) w momencie, gdy utwór się kończy,
automatyczne przejście na razie nie zadziała.

To świadomie odłożone uproszczenie na ten etap -- pełny mechanizm, niezależny
od tego, czy panel hosta jest akurat otwarty (np. licznik czasu utrzymywany
po stronie backendu, niezależnie od żadnej konkretnej przeglądarki), to
zaplanowane ulepszenie na później.

## ETAP 5 -- mikrofon na czas

Host daje komuś głos "na 5 sekund" (patrz ETAP 3). Ale kto właściwie pilnuje,
że po tych 5 sekundach mikrofon naprawdę się wyłączy? Odpowiedź: **serwer,
nigdy przeglądarka uczestnika.**

### Dlaczego to nie może być przeglądarka

Wydawałoby się najprościej: przeglądarka uczestnika sama sobie odlicza 5
sekund i sama wyłącza mikrofon, gdy czas minie. Problem w tym, że
przeglądarka jest **w rękach uczestnika** -- a każdy, kto otworzy narzędzia
deweloperskie (wbudowany w każdą przeglądarkę podgląd "od kuchni", pokazujący
i pozwalający zmieniać to, co strona robi), mógłby spróbować "oszukać" ten
odliczający zegar: zatrzymać go, cofnąć, albo w ogóle zignorować i dalej
nadawać dźwięk. Nie da się zaufać stoperowi, który leży po stronie osoby,
której ten stoper akurat ogranicza.

Dlatego czas mikrofonu pilnuje wyłącznie backend -- **serwer ma własny,
niezależny stoper**, do którego uczestnik nie ma żadnego dostępu.

### Jak to działa krok po kroku

1. Gdy host oddaje komuś mikrofon, backend zapisuje w bazie: kto, od kiedy,
   na jak długo.
2. Backend liczy czas **sam u siebie** -- nie czeka na żadną wiadomość od
   przeglądarki uczestnika typu "skończyłem mówić" czy "minęło 5 sekund".
   Gdyby czekał na taką wiadomość, wracalibyśmy do tego samego problemu:
   uczestnik mógłby jej po prostu nie wysłać.
3. Gdy czas minie (według zegara serwera, nie klienta), backend **sam z
   siebie** każe serwerowi LiveKit odebrać temu uczestnikowi prawo do
   nadawania dźwięku -- fizycznie, na poziomie serwera streamingu, a nie
   "grzecznościową" prośbą do przeglądarki.
4. Dopiero po tym backend rozsyła przez SignalR informację do wszystkich:
   "koniec, mikrofon wraca do hosta" -- to już tylko powiadomienie dla
   interfejsu, samo zablokowanie nadawania stało się wcześniej, w kroku 3.

### Dwa niezależne zabezpieczenia, nie jedno

To ważne rozróżnienie: backend nie polega na jednym mechanizmie, tylko na
dwóch, niezależnych od siebie:

- **Baza danych** -- mówi backendowi, kto aktualnie "powinien" mieć głos i
  do kiedy. To jest źródło decyzji ("czyja jest kolej").
- **Realny serwer streamingu (LiveKit)** -- to on fizycznie decyduje, czy
  dźwięk z czyjejś kamery/mikrofonu w ogóle przejdzie dalej do widzów.
  Nawet gdyby coś poszło nie tak z pierwszym zabezpieczeniem (np. błąd w
  logice backendu), LiveKit i tak nie puści dźwięku od kogoś, komu prawo do
  nadawania zostało odebrane.

Innymi słowy: gdyby jedno zabezpieczenie zawiodło, drugie i tak trzyma
granicę. Nie ma sytuacji, w której samo "zaufanie" do przeglądarki uczestnika
jest jedyną rzeczą stojącą między nim a mikrofonem na dłużej, niż powinien go
mieć.

### Audio ducking -- muzyka sama się przycisza, gdy ktoś mówi

Gdy ktoś dostaje mikrofon, w tym samym momencie muzyka **u wszystkich**
uczestników (nie tylko u tej jednej osoby, która akurat mówi) automatycznie,
delikatnie przycisza się w tle -- żeby było wyraźnie słychać mówiącego, a nie
tylko domyślać się go spod podkładu muzycznego. To dzieje się samo, bez
żadnej akcji ze strony widzów czy hosta.

Jak tylko dana osoba przestaje mieć mikrofon (czy to dlatego, że minął czas
z poprzedniej sekcji, czy host zabrał go wcześniej), muzyka u wszystkich
**wraca do pełnej głośności**, tak samo automatycznie.

Odpowiednik z życia: to jak DJ, który przycisza podkład, gdy ktoś bierze
mikrofon do ogłoszenia, i podkręca z powrotem, gdy ogłoszenie się kończy --
tylko że tu robi to system, jednocześnie w przeglądarkach wszystkich osób na
imprezie, żeby nikt nie usłyszał czegoś innego niż reszta.

## ETAP 6 -- czat

Czat leci tym samym kanałem co reszta zdarzeń sterujących -- przez SignalR
(patrz Przepływ 2). Nowość na tym etapie to nie kanał, tylko **kto w ogóle
może pisać** -- i to wymaga osobnego wyjaśnienia, bo różni się od tego, jak
działa dołączanie do poczekalni/sceny (ETAPY 2-3).

### Osobna, lekka tożsamość -- inna niż na scenie

Dołączenie do poczekalni/sceny (ETAP 2) to dość ciężka procedura: zgoda na
kamerę, przejście przez `/join`, token LiveKit. To ma sens dla kogoś, kto ma
się pojawić na scenie z obrazem i dźwiękiem -- host musi mieć nad tym
kontrolę.

Ale na czacie pisać ma prawo **każdy widz**, nie tylko ci, którzy przeszli
przez poczekalnię i trafili na scenę. Zmuszanie kogoś do włączenia kamery i
czekania na akceptację hosta tylko po to, żeby mógł napisać jedno zdanie na
czacie, byłoby bez sensu i zniechęcałoby ludzi, którzy chcą tylko oglądać.

Dlatego czat ma **własną, osobną, lekką tożsamość**, niezależną od tej ze
sceny/poczekalni:

| | Tożsamość sceny/poczekalni (ETAP 2-3) | Tożsamość czatu (ETAP 6) |
|---|---|---|
| Co trzeba zrobić | Wejść przez `/join`, zgodzić się na kamerę | Podać nick |
| Co dostaje przeglądarka | Token LiveKit -- przepustka do nadawania obrazu | Identyfikator sesji czatu -- zapamiętany tylko w tej przeglądarce |
| Kto może | Tylko osoby wpuszczone przez hosta | Każdy widz na stronie imprezy |
| Do czego to prawo | Nadawanie obrazu/dźwięku na scenę | Pisanie wiadomości na czacie |

Identyfikator sesji czatu to coś jak numerek wydany przy wejściu na widownię
-- nie wymaga kamery ani decyzji hosta, tylko podania nicku. Przeglądarka
zapamiętuje go u siebie, żeby backend wiedział, że kolejne wiadomości
przychodzą od tej samej osoby (np. do liczenia rate limitingu, patrz niżej).
To rozróżnienie jest świadome: nawet ktoś, kto **nigdy** nie przeszedł przez
`/join` i nie ma żadnego tokenu LiveKit, spokojnie pisze na czacie -- bo to
dwa zupełnie osobne światy, z osobnymi tożsamościami.

### Ograniczenie prędkości (rate limiting) -- prosto o co chodzi

Ograniczenie prędkości (po angielsku *rate limiting*) to jak kolejka przy
jednych drzwiach: jeśli ktoś próbuje przez nie przepychać się co sekundę, w
kółko, ochroniarz go na chwilę zatrzymuje -- nie dlatego, że coś jest nie tak
z drzwiami, tylko żeby ta jedna osoba nie zablokowała wejścia wszystkim
innym.

Na czacie działa to tak samo:

- Backend liczy, jak często dana tożsamość czatu (patrz wyżej) wysyła
  wiadomości.
- Jeśli ktoś próbuje pisać za szybko -- np. spamuje tym samym zdaniem w
  kółko albo jakiś skrypt próbuje zalać czat -- backend **chwilowo**
  wstrzymuje kolejne wiadomości od tej osoby, zamiast rozsyłać je dalej
  przez SignalR do wszystkich.
- To nie jest ban ani wyrzucenie z imprezy -- to tymczasowe "poczekaj
  chwilę, zanim napiszesz kolejną wiadomość". Po krótkiej przerwie ta sama
  osoba znów pisze normalnie.
- Chroni to dwie rzeczy naraz: widzów (żeby ekran nie zalał się spamem) i
  sam kanał SignalR (żeby jedna osoba nie potrafiła go przeciążyć --
  pamiętaj, że tym samym kanałem leci też sterowanie sceną i mikrofonem,
  patrz Przepływ 2).

### Treść wiadomości -- zawsze zwykły tekst

Wiadomość czatu, zanim trafi do innych przeglądarek, jest oczyszczana
(sanityzowana) -- inaczej ktoś mógłby wkleić fragment kodu zamiast zwykłego
tekstu, który wykonałby się w przeglądarkach innych widzów. Backend zawsze
rozsyła samą treść jako zwykły tekst do wyświetlenia, nigdy jako coś, co
przeglądarka miałaby "wykonać". Szczegóły w `BEZPIECZENSTWO.md`.

## ETAP 7 -- bezpieczeństwo

Kilka rzeczy odłożonych świadomie we wcześniejszych etapach (patrz notatki przy
ETAPIE 2 i ETAPIE 3 wyżej) zostało domkniętych w tym etapie: autoryzacja
połączeń SignalR do grupy hosta, twarde usunięcie ze sceny przez LiveKit Room
Service (fizyczne odebranie prawa nadawania, nie tylko zmiana w bazie), ogólny
rate limiting API (z surowszym limitem na logowanie), blokada konta po
nieudanych próbach logowania, nagłówki bezpieczeństwa (CSP i pokrewne) oraz
limit rozmiaru requestów.

**Jedno ważne zastrzeżenie do CSP:** nagłówek `Content-Security-Policy` jest
dziś doklejany do odpowiedzi backendu (API), ale docelowo stronę SPA (pliki
frontendu) serwuje Nginx jako pliki statyczne -- backend nigdy nie widzi tego
żądania, więc nie może doklejić do niego nagłówka. Ten sam nagłówek trzeba więc
skonfigurować **także w Nginx** (opisane w `VPS.md`) -- do czasu wdrożenia na
VPS dodatkową, słabszą warstwą jest znacznik CSP wpisany wprost w
`frontend/index.html`.

Pełna, aktualna lista tego, co chroni system, jest w `BEZPIECZENSTWO.md`.

## ETAP 8 -- optymalizacja i monitoring

Ten etap to "domykanie" pod kątem większej liczby osób na imprezie naraz oraz
tego, żeby dało się z zewnątrz sprawdzić, czy serwer w ogóle żyje -- bez
włączania przeglądarki i klikania po stronie.

### Simulcast -- host wysyła obraz w kilku jakościach naraz

Simulcast to sytuacja, w której przeglądarka nadająca obraz (host albo
uczestnik na scenie) wysyła do LiveKit **kilka wersji tego samego obrazu
naraz** -- np. w wysokiej, średniej i niskiej rozdzielczości. LiveKit sam
decyduje, którą wersję wysłać danemu widzowi, zależnie od tego, jak duże jest
jego okienko wideo na ekranie i jak dobre ma łącze -- dokładnie tak, jak
YouTube samo dobiera jakość (1080p/720p/480p) do tego, co akurat oglądasz i
jak szybki masz internet.

Bez tego każdy widz dostawałby dokładnie tę samą, jedną jakość -- albo za
ciężką dla kogoś na słabym łączu (obraz się zacina), albo bez sensu wysoką
dla kogoś, kto ogląda scenę w małym okienku na telefonie.

**Stan faktyczny:** to jest już aktywne, bez żadnego dodatkowego kodu po
naszej stronie -- biblioteka kliencka LiveKit (`livekit-client`), której
używa frontend, ma to włączone **domyślnie** przy publikowaniu obrazu z
kamery (sprawdzone wprost w kodzie biblioteki: `publishDefaults.simulcast =
true`).

### Adaptive stream -- automatyczne dobieranie jakości, bez klikania

To w zasadzie druga połowa tego samego mechanizmu, tylko po stronie
odbiorcy (widza), nie nadawcy. "Adaptive stream" (strumień dopasowujący się
automatycznie) to funkcja LiveKit, która **sama** obserwuje, jak duże jest
akurat okienko wideo w przeglądarce danego widza (np. mała miniaturka na
liście kontra pełny ekran) i **sama** prosi LiveKit o dopasowaną jakość --
widz nic nie klika, nie ma żadnego przycisku "zmień jakość".

**Stan faktyczny: na razie WYŁĄCZONE.** Biblioteka LiveKit ma tę opcję
domyślnie wyłączoną (`adaptiveStream: false`), a we frontendzie
(`LiveKitSerwis.ts`) pokój LiveKit jest tworzony bez żadnych dodatkowych
ustawień, więc nigdzie nie jest włączana. Efekt: simulcast (wyżej) już
przygotowuje kilka jakości do wyboru, ale nic jeszcze automatycznie nie
wybiera tej dopasowanej do wielkości okienka widza. To jest do zrobienia w
kolejnym kroku -- jedna linijka w `LiveKitSerwis.ts` (`new Room({
adaptiveStream: true })`).

### Reconnect -- co się dzieje, gdy komuś wypadnie internet

W systemie są trzy oddzielne rzeczy, które muszą "przeżyć" chwilową utratę
internetu -- każda działa inaczej:

1. **Kanał sterowania (SignalR)** -- przeglądarka sama próbuje połączyć się
   ponownie (`.withAutomaticReconnect()`), bez żadnej akcji użytkownika. Po
   powrocie połączenia widz automatycznie wraca do grupy "widzowie" i dalej
   dostaje zdarzenia na żywo.
2. **Obraz/dźwięk (LiveKit)** -- biblioteka LiveKit ma wbudowany własny
   mechanizm ponownego łączenia, który działa sam, bez dodatkowego kodu.
   Panel hosta pokazuje to nawet na ekranie (stan "łączenie" / "połączono"),
   widz na razie takiego komunikatu nie widzi, ale mechanizm pod spodem
   działa tak samo.
3. **Muzyka** -- gdy przeglądarka wraca po przerwie, a pozycja w utworze
   rozjechała się o 2 sekundy lub więcej (typowe po dłuższym zerwaniu
   połączenia), `AudioSerwis` robi natychmiastowy skok do właściwego miejsca
   zamiast czekać, aż dogoni różnicę drobną korektą tempa (patrz ETAP 4) --
   inaczej wracalibyśmy do muzyki "z przeszłości".

Dzięki temu **scena u innych się nie rozsypuje** -- gdy komuś wypadnie i
wróci internet, host nie musi go na nowo przeciągać na slot: jego miejsce na
scenie w bazie nadal jest zajęte, więc jak tylko wróci połączenie, obraz i
dźwięk po prostu doskakują z powrotem w to samo miejsce.

#### Notatka: jedna rzecz na razie NIE wraca sama -- panel hosta po reconnect

Jest jeden konkretny wyjątek, o którym trzeba wiedzieć: gdy hostowi zerwie
się i wróci połączenie SignalR, jego przeglądarka **automatycznie NIE
dołącza ponownie do grupy "host"** -- każde nowe połączenie (również to po
reconnect) startuje od zera w grupie "widzowie", a wejście do grupy "host"
wymaga jawnego wywołania `DolaczDoGrupyHosta()`. Dziś frontend wywołuje to
tylko raz, przy pierwszym wejściu na panel hosta -- nie robi tego ponownie po
odzyskaniu połączenia. W praktyce: gdyby hostowi na chwilę wypadł internet w
trakcie prowadzenia imprezy, po powrocie połączenia mógłby przestać dostawać
na żywo zdarzenia panelu (np. zmiany w poczekalni), dopóki nie odświeży
strony ręcznie. To jest znane ograniczenie, opisane wprost w komentarzu w
kodzie (`StanImprezyHub.cs`) -- do domknięcia: podpięcie
`DolaczDoGrupyHosta()` też pod zdarzenie `onreconnected()` połączenia
SignalR we frontendzie.

### Monitoring -- jak sprawdzić z zewnątrz, czy serwer żyje

- **Logi** -- Serilog, skonfigurowany od pierwszej linijki startu API:
  każda ważna operacja trafia jednocześnie na konsolę i do pliku w
  `SameBeat.Api/logs/`, z rotacją co dzień (30 dni wstecz). Zasada: nigdy nie
  logujemy haseł ani tokenów (JWT, LiveKit) -- ani w treści komunikatu, ani
  jako dodatkowe pole logu.
- **`GET /api/health`** -- publiczny endpoint bez logowania, do podpięcia
  pod zewnętrzny monitoring (np. uptime-kuma). Poza samym "żyję"
  (status/wersja/czas) pokazuje też: czy baza danych odpowiada (i w ilu ms),
  ile jest otwartych połączeń SignalR, i jak długo proces backendu już
  działa (uptime).
- **`GET /api/host/diagnostyka`** -- to samo co wyżej, ale tylko dla
  zalogowanego hosta (chronione tak samo jak reszta `/api/host/...`), z
  rozbiciem liczby połączeń SignalR na "widzowie" / "host".
- **Czego tu celowo nie ma:** jakość samego połączenia wideo/audio
  (opóźnienia, utrata pakietów, bitrate) -- to mierzy LiveKit po swojej
  stronie; integrowanie tego z `/api/health` było poza zakresem MVP.

## Powiązane dokumenty

- `INSTALACJA.md` -- jak to odpalić lokalnie do testów.
- `VPS.md` -- plan wdrożenia na docelowy serwer produkcyjny.
- `BEZPIECZENSTWO.md` -- pełny obraz zabezpieczeń: co działa, co jeszcze nie.
- `GOTOWOSC_MVP.md` -- checklist kryteriów gotowego MVP, punkt po punkcie.
