# Raport: „Mecz w trakcie" — oznaczenie klubów grających teraz

Gałąź: `mecz-w-trakcie`. Zadanie: pokazać na stronie meczu, czyj klub gra
właśnie teraz, na podstawie terminarza `fixtures/?event={numer}`.

## Zmiany plik po pliku

### Nowe pliki

**`src/Fpl/Api/Dto/FixtureDto.php`**
DTO jednego meczu terminarza: `teamH`, `teamA`, `started`, `finishedProvisional`.
Metoda `isPlayingNow()` niesie regułę zamówioną przez właściciela: mecz gra
teraz, gdy `started = true` i `finished_provisional = false`. Reguła siedzi
w jednym miejscu (DTO), żeby nikt nie musiał jej powielać przy każdym
odczycie.

**`src/Fpl/Api/FplApiClient.php` (metoda `fixtures()`)**
Nowa metoda pobiera `fixtures/?event={gw}` i mapuje listę na `list<FixtureDto>`.
Trzyma się konwencji ustalonej w pliku: rzuca `FplApiException` przy 404,
bo poprawny numer kolejki zawsze zwraca listę (pustą przed startem sezonu),
więc 404 to realna awaria, nie normalny stan — analogicznie do `live()`.

**`src/Fpl/PlayingClubsResolver.php`**
Nowa usługa. Dla danej kolejki zwraca zbiór id klubów PL grających właśnie
teraz (`forGameweek(Gameweek): array<int, true>`) oraz wygodny skrót
`isPlaying(Gameweek, int): bool`. Trzy zasady wpisane w kod (i w komentarz
dokładnie nad klasą, żeby nie dało się o nich zapomnieć):
- kolejka zamrożona (`data_checked`) w ogóle nie odpytuje API — sprawdzane
  jako pierwsze, przed jakimkolwiek zapytaniem;
- `FplApiException` jest łapany w tej klasie, logowany jako `warning` i
  kończy się pustym zbiorem — nigdy nie ucieka wyżej, więc strona renderuje
  się bez oznaczeń zamiast się wywrócić;
- wynik jest pamiętany w prywatnej tablicy `$cache` (klucz = numer kolejki)
  na czas życia obiektu usługi. Usługi Symfony są dzielone (singleton) w
  obrębie jednego żądania HTTP, więc kilka meczów jednej strony kolejki,
  przechodzących przez `MatchViewBuilder::build()`, odpytuje terminarz
  dokładnie raz — ten sam mechanizm, który `docs/do-zrobienia.md` już
  postuluje dla `event/{gw}/live/`.

**Gdzie umieściłem usługę i dlaczego (`src/Fpl/` a nie `src/League/`):**
`PlayingClubsResolver` zależy wyłącznie od `FplApiClient` i encji `Gameweek`
— nie zna `MatchView`, formatów meczu ani żadnego pojęcia z domeny ligowej.
To czysto techniczna usługa dostępu do jednego zasobu FPL z pamięcią na
czas żądania i regułą odporności na awarię, tego samego gatunku co
`EntryGameweekSync` i `DictionarySync` z `src/Fpl/Sync/`. Nie wrzuciłem jej
do `src/Fpl/Sync/`, bo nic tam nie zapisuje do bazy — nazwa „Sync" byłaby
myląca. Umieściłem ją bezpośrednio w `src/Fpl/` (obok katalogów `Api/` i
`Sync/`), tam gdzie już mieszka `FplApiException`. `App\League\MatchViewBuilder`
wstrzykuje ją jak każdą inną zależność.

**`tests/Unit/Fpl/PlayingClubsResolverTest.php`**
Testy jednostkowe usługi: klub grający i niegrający w tej samej kolejce,
nieznany klub jako niegrający, kolejka zamknięta niepytająca API (`assertSame(0, ...)`),
awaria zapytania niewywracająca wywołania, oraz jedno zapytanie HTTP mimo
kilku wywołań `forGameweek()`/`isPlaying()`.

**`tests/fixtures/api/fixtures_gw1.json`**
Atrapa odpowiedzi terminarza: klub 1 i 2 grają teraz (`started=true,
finished_provisional=false`), klub 3 i 4 skończyły (`finished_provisional=true`),
klub 5 i 6 jeszcze nie zaczęły (`started=false`).

### Zmienione pliki

**`src/League/MatchView.php`**
Nowe pole `array $playingClubIds` (domyślnie `[]`, zbiór id klubów) i metoda
`isClubPlayingNow(int $plTeamId): bool`, którą szablon woła jednym wyrażeniem.

**`src/League/MatchViewBuilder.php`**
Wstrzyknięty `PlayingClubsResolver`. `build()` woła
`$this->playingClubs->forGameweek($gameweek)` i przekazuje wynik do
`MatchView`. Jedyne miejsce w `src/` konstruujące `MatchView` — poprawione.

**`templates/fixture/_chip.html.twig`**
Makro `row(pick, live, playingNow = false)` — nowy, domyślny parametr, więc
nie psuje innych wywołań. Gdy `playingNow`, tuż za skrótem klubu (`.club`)
pojawia się `<span class="live-dot" title="Klub gra teraz">▶</span>`.

**`templates/fixture/_picks.html.twig`**
Jedyne miejsce wołające `pick_row.row(...)` z realnymi danymi zawodnika —
dopisany trzeci argument: `view.isClubPlayingNow(pick.element.plTeam.id)`.

**`public/css/app.css`**
Nowa reguła `.squad-picks .live-dot` i `@keyframes live-dot-pulse`.

**`tests/Support/FplMockHttpClient.php`**
Dodany wariant dla ścieżek `/fixtures/` (zwraca `fixtures_gw1`), żeby testy
funkcjonalne (kontener testowy `fpl.client`) miały czym odpowiedzieć na nowe
zapytanie zamiast zawsze dostawać 404.

**`tests/Unit/Fpl/FplApiClientTest.php`, `tests/Integration/MatchViewBuilderTest.php`,
`tests/Functional/MatchControllerTest.php`**
Nowe testy opisane niżej.

## Dowód: testy padały przed implementacją

Przed dopisaniem `FplApiClient::fixtures()` i klasy `PlayingClubsResolver`
uruchomienie nowych testów kończyło się błędami klasy/metody, np.:

```
Error: Class "App\Fpl\PlayingClubsResolver" not found
Error: Call to undefined method App\Fpl\Api\FplApiClient::fixtures()
```

(21 błędów w `tests/Unit/Fpl/FplApiClientTest.php` + `PlayingClubsResolverTest.php`
+ `MatchViewBuilderTest.php` przy pierwszym uruchomieniu.)

Test szablonu (`testMarksPlayerFromClubPlayingNowButNotFromFinishedClub`)
napisany i uruchomiony przed zmianą `_chip.html.twig`/`_picks.html.twig`
padł tak:

```
Klub grajacy teraz ma dostac oznaczenie.
Failed asserting that actual size 0 matches expected size 1.
```

Po implementacji wszystkie te testy przechodzą.

## Wynik całej suity

```
vendor/bin/phpunit
OK (281 tests, 965 assertions)
```

(268 istniejących + 13 nowych: 3 w `FplApiClientTest`, 5 w
`PlayingClubsResolverTest`, 4 w `MatchViewBuilderTest`, 1 w `MatchControllerTest`.)

## Testy, o których warto wspomnieć

- `PlayingClubsResolverTest::testMarksPlayingClubAndLeavesFinishedClubOut` —
  klub grający i klub, który skończył mecz, w tej samej kolejce.
- `PlayingClubsResolverTest::testClosedGameweekNeverQueriesApi` — kolejka
  zamknięta (`data_checked`) nie robi żadnego zapytania (`assertSame(0, ...)`).
- `PlayingClubsResolverTest::testFailedFixturesRequestDoesNotBreakAndReturnsEmptySet`
  oraz `MatchViewBuilderTest::testFailedFixturesRequestStillBuildsViewWithoutMarks` —
  awaria zapytania o terminarz nie przerywa budowy widoku, zwraca pusty zbiór.
- `PlayingClubsResolverTest::testQueriesApiOnlyOncePerGameweekAcrossMultipleCalls`
  oraz `MatchViewBuilderTest::testFixturesEndpointIsQueriedOnceAcrossMultipleMatchesInTheSameGameweek` —
  jedno zapytanie HTTP mimo kilku wywołań/kilku meczów jednej kolejki.
- `MatchControllerTest::testMarksPlayerFromClubPlayingNowButNotFromFinishedClub` —
  pełny przejazd przez kontroler i szablon: zawodnik klubu grającego dostaje
  `.live-dot` z tytułem „Klub gra teraz", zawodnik klubu, który skończył mecz — nie.

## Oznaczenie w składzie — jak wygląda i dlaczego

Tuż za skrótem klubu (`<span class="club">ARS</span>`) pojawia się, wyłącznie
gdy klub gra teraz, mały trójkąt: `<span class="live-dot" title="Klub gra
teraz">▶</span>`.

- **Cichość**: brak własnego tła czy obrysu (w przeciwieństwie do `.pts` czy
  `.cap`) — to 8px znaczek w kolorze `var(--mine)`, nie konkuruje wzrokowo
  z wartością punktową w kolumnie `.num` ani ze skrótami statystyk w `.stats`.
- **Czytelność bez najeżdżania myszą**: znaczek jest zawsze widoczny (nie
  tylko w `title`), `title="Klub gra teraz"` to tylko dodatkowe potwierdzenie
  po polsku przy hover/screen readerze.
- **Nie tylko kolor**: sam fakt istnienia trójkąta obok skrótu klubu (a nie
  jego brak) jest sygnałem niezależnym od koloru — widoczny też w skali
  szarości. Kolor `var(--mine)` (bursztyn) jest sygnałem dodatkowym, ponownym
  użyciem tokenu już używanego do oznaczeń przy zawodniku (np. plakietka
  kapitana `.cap`), więc nie wprowadza nowego znaczenia barwy w tym widoku.
- **Zmienne z góry `app.css`**: kolor to `var(--mine)`, bez żadnej nowej,
  twardo wpisanej wartości.
- **Zero JS, animacja z poszanowaniem `prefers-reduced-motion`**: subtelne
  pulsowanie (`@keyframes live-dot-pulse`, czysty CSS) jest już objęte
  istniejącą, globalną regułą na dole `app.css`:
  `@media (prefers-reduced-motion: reduce) { * { animation: none !important; ... } }`
  — nie trzeba było dopisywać nic dodatkowego, ta reguła używa selektora `*`.

## Wątpliwości / decyzje warte odnotowania

- Endpoint `fixtures/?event=` w atrapie testowej (`FplMockHttpClient`) dopasowuję
  po samej ścieżce (`/fixtures/`), bez rozróżniania numeru kolejki w query
  stringu — tak samo jak już traktowane są tam `picks`/`live`. W realnym API
  numer kolejki oczywiście się liczy (to obsługuje `FplApiClient::fixtures()`).
- Pole `kickoff_time` i `minutes` ze specyfikacji endpointu świadomie pominąłem
  w `FixtureDto` — zadanie zaznaczyło je jako „mogą się przydać", ale reguła
  „gra teraz" ich nie potrzebuje, a zasada ograniczania zakresu każe nie
  dodawać nieużywanych pól.
