nginx nasłuchuje wyłącznie na IPv4, a w obrazie alpine localhost rozwiązuje
się najpierw na ::1. Healthcheck kończył się więc odmową połączenia i
kontener nigdy nie osiągał stanu healthy - wyszło to przy pisaniu CI,
gdzie ten sam sposób odpytania nie działał.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Konfiguracja we flat configu: zalecane zestawy js, typescript-eslint oraz
eslint-plugin-vue. Bloki <script setup lang="ts"> obsługuje vue-eslint-parser
przekazujący zawartość do parsera TypeScriptu.
Odstępstwa od domyślnych reguł:
- vue/multi-word-component-names wyłączone - kafelki panelu mają nazwy
własne i nie każda jest dwuczłonowa
- no-unused-vars przepuszcza nazwy z prefiksem podkreślenia
- no-console wyłączone dla testu dymnego, który raportuje wynik na stdout
Szablony przepuszczone przez eslint --fix, co ujednoliciło łamanie
atrybutów i domykanie znaczników. Po zmianach kontrola typów, build
i test dymny nadal przechodzą.
Dochodzą skrypty npm: lint (z --max-warnings 0) oraz lint:fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Workflow .gitea/workflows/backend.yaml z trzema jobami:
- lint ruff check oraz ruff format --check --diff
- test testy jednostkowe i kontrakt API, plus compileall całego pakietu
- image testowy build obrazu; job czeka na lint i test
Job image nie poprzestaje na zbudowaniu obrazu - uruchamia kontener bez
prawdziwego tokenu Discorda i czeka, aż /api/status odpowie. Panel łączy
się z backendem po sieci, więc sam start procesu niczego nie dowodzi.
Zapytanie leci z wnętrza kontenera, dzięki czemu krok nie zależy od tego,
czy runner ma zainstalowanego curla albo wgeta.
Filtry paths ograniczają uruchomienia do zmian w kodzie Pythona,
zależnościach, konfiguracji lintera, Dockerfile i samym workflow.
Zależności instalują się z gotowych kółek, więc obraz CI nie potrzebuje
kompilatora. ffmpeg i libopus są wymagane dopiero przy odtwarzaniu,
a testy nie dotykają warstwy audio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Panel jest osobnym klientem HTTP, więc zmiana ścieżki albo metody psuje go
po cichu - w kodzie Pythona nic nie przestaje się kompilować. Test buduje
aplikację, generuje specyfikację OpenAPI i porównuje ją z listą 22
endpointów w obie strony:
- brak oczekiwanego endpointu to błąd
- nowy endpoint spoza listy również, żeby rozszerzenie API było świadomą
decyzją, a nie skutkiem ubocznym
Sprawdzany jest też sam dokument OpenAPI, co wyłapuje błędy w deklaracjach
tras i modelach żądań.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ruff jako linter i formatter, konfiguracja w pyproject.toml:
- reguły E, W, F, I, N, UP, B, C4, SIM, TID, BLE, RUF
- extend-immutable-calls dla fastapi.Depends, File i Query - wstrzykiwanie
zależności opiera się na wywołaniach w wartościach domyślnych argumentów,
więc bugbear B008 zgłaszał tam 66 fałszywych trafień
- zakaz importów względnych, aby ścieżki w pakiecie czytało się jednoznacznie
- katalog frontend wyłączony z analizy
Poprawki, które wskazał linter:
- Awaitable, Callable i AsyncIterator importowane z collections.abc zamiast
z typing
- zbędny argument trybu w open(path, "r")
- contextlib.suppress zamiast try/except/pass przy anulowaniu zadania
- zdjęte dwie niepotrzebne dyrektywy noqa
- pięć linii przekraczających 100 znaków zawiniętych
Całość przepuszczona przez ruff format. Do .gitignore dochodzi .ruff_cache/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opisane uruchamianie, zmienne środowiskowe, testy oraz mapa pakietu
z klasą przypisaną do pliku. Spisane też trzy zasady, które trzymają
strukturę w ryzach: MusicPlayer jest jedynym właścicielem stanu, kolejka
jest nieusuwalna i śledzona indeksem, a zależności idą przez konstruktory
zamiast globalnych singletonów.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kolejka jest nieusuwalna, a pozycję śledzi indeks, więc przeskoki,
usuwanie i zmiana kolejności muszą ten indeks poprawnie przesuwać. To
najbardziej podatna na błędy część przeniesionego kodu, więc dostaje
testy przypinające zachowanie:
- start odtwarzania przy dodaniu do pustej kolejki
- limit odtworzeń usuwający utwór i cofający indeks
- przechodzenie dalej oraz oba tryby zapętlania
- koniec kolejki czyszczący aktualny utwór
- przeskok, usunięcie utworu przed indeksem i usunięcie odtwarzanego
- zmiana kolejności zachowująca indeks na odtwarzanym utworze
- stosowanie głośności serwera i zatrzymanie przy niedziałających utworach
16 przypadków na atrapach Discorda, bez sieci i ffmpega. Uruchamiane
przez python -m unittest discover -s tests -t . - bez nowych zależności.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Utwór, dla którego nie udało się zbudować świeżego źródła, był pomijany
przez rekurencyjne wywołanie play_next(advance=True). Przy włączonym
zapętlaniu kolejki - a przy zapętlaniu jednego utworu tym bardziej -
kolejny indeks wskazywał znowu na utwór niedziałający, więc pomijanie
kręciło się w kółko, zasypując kanał powiadomieniami.
Licznik nieudanych prób wędruje teraz przez play_next. Po liczbie
niepowodzeń równej długości kolejki odtwarzanie zatrzymuje się z jednym
komunikatem zamiast pętli.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
YTDLSource powstaje z domyślną głośnością 0.5, a ustawienie głośności
zmieniało tylko źródło aktualnie odtwarzane. Nowy utwór startował więc
na 50%, podczas gdy panel i /api/status raportowały stan serwera - domyślnie
100%. Zmiana głośności "znikała" po przejściu do kolejnego utworu.
Teraz play_next ustawia na źródle głośność ze stanu serwera, zanim
przekaże je do odtworzenia.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Application składa wszystkie zależności w jednym miejscu i prowadzi bota
równolegle z serwerem web. Panel przeżywa awarię logowania do Discorda,
tak jak dotąd, ale komunikat idzie przez logging zamiast print.
- zyrarbot/__main__.py pozwala uruchomić pakiet przez python -m zyrarbot
i konfiguruje logowanie; main.py zostaje cienkim punktem wejścia.
- Usunięte bot.py, web.py i ytdl.py - ich zawartość rozeszła się po
pakiecie. Zniknął przy tym globalny obiekt bota tworzony przy imporcie
modułu, który wymuszał kolejność importów między bot.py a web.py.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
22 endpointy z jednego pliku rozdzielone na routery według obszaru:
status, playback, queue, voice, uploads, settings i websocket.
- Powtarzany w każdym endpoincie blok "pobierz cog, sprawdź czy istnieje,
ustal aktywny serwer, w razie czego rzuć 400" zastąpiony zależnościami
z dependencies.py. Zniknęło 22-krotne bot.get_cog("MusicBot").
- Zależności wędrują przez app.state zamiast importu globalnego obiektu
bota, więc warstwa web nie ma już zależności cyklicznej z modułem bota.
- create_app składa aplikację i montuje routery.
- Wycofane @app.on_event("startup") zastąpione lifespanem, który dodatkowo
sprząta po sobie: odpina hook i zatrzymuje pętlę rozgłaszania.
Kontrakt HTTP pozostaje bez zmian - te same ścieżki, metody, kody błędów
i kształt odpowiedzi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ConnectionManager - rejestr aktywnych połączeń (bez zmian poza typami).
- StatusService - budowanie obrazu stanu bota. Endpoint get_status miał
126 linii i składał odpowiedź w dwóch wariantach; teraz jest to zestaw
metod budujących poszczególne fragmenty payloadu.
- StatusBroadcaster - rozgłaszanie na żądanie oraz pętla cykliczna, wraz
z zatrzymywaniem zadania przy wyłączaniu aplikacji. Wcześniej zadanie
było tworzone w on_event("startup") i nigdy nie anulowane.
Wariant odpowiedzi dla bota bez serwerów zwraca teraz komplet kluczy
(dochodzi queue_index: -1), więc panel dostaje zawsze ten sam kształt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jedenaście klas pydantic zadeklarowanych jednym blokiem na górze web.py
rozdzielone na pliki zgodnie z zasadą jedna klasa = jeden plik. Każdy
model dostaje docstring mówiący, do czego służy - z samych nazw pól
(np. channel_id akceptujące pusty string jako reset) nie wynikało to
w oczywisty sposób.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ZyrarBot buduje intencje, rejestruje cog i synchronizuje drzewo komend.
- MusicCog zawiera już wyłącznie obsługę komend ukośnikowych: walidację
argumentów, komunikaty dla użytkownika i wywołania MusicPlayer. Cała
praca ze stanem zniknęła z tego pliku.
- GuildResolver ustala kontekst serwera dla panelu webowego, który nie ma
interakcji Discorda - wcześniej robiła to funkcja get_active_guild
w web.py, sięgająca po globalny obiekt bota.
- Powtarzana w czterech komendach odpowiedź "dodano do kolejki" albo
"teraz odtwarzane" zebrana w jednym helperze.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Logika odtwarzania wyprowadzona z coga Discorda do osobnej klasy. Dotąd
kolejkę modyfikowały dwie niezależne ścieżki: metody MusicBot oraz
endpointy w web.py sięgające po music_cog.queues[guild_id] i ustawiające
music_cog.jump_flag wprost. Teraz obie warstwy wołają MusicPlayer.
Przeniesione bez zmiany zachowania: enqueue, play_next wraz z arytmetyką
indeksu i limitem odtworzeń, remove, reorder, jump, sterowanie głośnością
oraz hook on_state_change rozgłaszający stan po WebSockecie.
Wybór kolejnego indeksu wydzielony do _next_index, dzięki czemu reguły
trybów zapętlania i przeskoku dają się czytać bez śledzenia całej metody.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- GuildMusicState zbiera stan jednego serwera w jedną strukturę. Dotąd
było to dziewięć równoległych słowników indeksowanych guild_id
(queues, currents, volumes, loop_currents, loop_playlists, last_channels,
history, queue_indices, jump_flag), co wymagało pamiętania, że przy
każdej zmianie trzeba ruszyć kilka z nich naraz.
- LoopMode zastępuje parę booleanów loop_current/loop_playlist, których
kombinacja "oba włączone" nie miała sensu, a mimo to była osiągalna.
Wartości enuma (off/playlist/single) odpowiadają kontraktowi API panelu.
- TrackEmbedFactory zbiera budowanie osadzeń Discorda, powielone dotąd
w enqueue_track i play_next.
- NotificationChannelResolver opisuje kolejność szukania kanału logów:
zapamiętany -> z konfiguracji -> pierwszy zapisywalny na serwerze.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ytdl.py rozbity na cztery pliki o rozłącznych rolach:
- YtdlOptions opcje yt-dlp i FFmpeg, wcześniej moduły-globalne
- MediaProbe odczyt długości pliku przez ffprobe
- YTDLSource samo źródło audio - dane utworu i zliczanie odtworzonego
czasu; dochodzą właściwości is_local, filepath, display_url
i has_web_link, które zastępują powtarzane w kilku miejscach
data.get("webpage_url") or url
- TrackFactory budowanie źródeł z zapytania, adresu lub pliku
Rozdzielenie źródła od sposobu jego tworzenia usuwa globalną instancję
YoutubeDL i pozwala podmienić fabrykę w testach. Metoda recreate zbiera
w jednym miejscu odtwarzanie utworu od nowa z zachowaniem jego tożsamości
i pozostałej liczby odtworzeń.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dostęp do danych trwałych schodzi za trzy repozytoria, zamiast być
rozsmarowany po metodach coga i endpointach:
- GuildConfigRepository - kanały powiadomień per serwer i domyślna liczba
odtworzeń.
- HistoryRepository - historia per serwer z buforem w pamięci i limitem
wpisów. Zasada odsiewania duplikatów (zgodny tytuł albo adres) siedzi
teraz w HistoryEntry.is_same_track_as zamiast w warunku wbudowanym
w listę składaną.
- UploadsRepository - katalog uploadów. Każda nazwa przechodzi przez
os.path.basename w jednym miejscu, co zamyka temat ścieżek względnych
przychodzących z API.
Wpisy historii i pliki opisują struktury HistoryEntry oraz UploadFileInfo
zamiast luźnych słowników z kluczami sprawdzanymi przez .get().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Początek pakietu zyrarbot, budowanego wedle zasady jedna klasa = jeden plik.
- Settings zbiera konfigurację ze zmiennych środowiskowych oraz ścieżki
i limity, które dotąd były literałami rozsianymi po bot.py i web.py
("config/config.json", "uploads", zakres 1-999, domyślne 5 odtworzeń).
- JsonFileStore zastępuje sześć niemal identycznych bloków open/json.load
z ręcznym try/except. Brakujący lub uszkodzony plik daje pusty słownik
zamiast wyjątku, a zapis sam tworzy katalog.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README opisuje pracę lokalną (npm run dev z proxy na backend), budowanie,
uruchomienie kontenera oraz strukturę katalogu src z rolą każdego pliku.
Spisane też zasady, które utrzymują panel spójnym: stan płynie z serwera,
zero innerHTML, ikony jako komponenty zamiast skryptu z CDN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ładuje artefakt z dist/ w jsdom, podstawiając atrapy WebSocketu i fetcha,
a następnie sprawdza dwie rzeczy naraz:
- czy dane z serwera faktycznie renderują się w interfejsie (aktualny utwór,
kolejka, historia, baza plików, głośność, tryb zapętlania, odmiana
liczebników, formatowanie czasu)
- czy kliknięcia trafiają we właściwe endpointy - każdy przycisk sterujący
porównywany jest z oczekiwaną parą metoda + ścieżka
Razem 31 sprawdzeń, uruchamiane przez npm run smoke po npm run build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rozdzielenie na dwie niezależne usługi:
- frontend/Dockerfile buduje panel w etapie node:22-alpine, a artefakt
serwuje nginx:1.27-alpine. Obraz nie zawiera Node'a ani źródeł.
- frontend/nginx.conf serwuje aplikację jednostronicową (nieznane ścieżki
wracają jako index.html), cache'uje /assets z sumą kontrolną w nazwie,
a /api i /ws przekazuje do usługi discord-bot. Przeglądarka widzi jedno
źródło, więc backend nie potrzebuje CORS-u.
Adres backendu siedzi w zmiennej nginxa, więc nazwa jest rozwiązywana
przy żądaniu i panel wstaje niezależnie od kolejności startu kontenerów.
- Proxy nie limituje rozmiaru ciała żądania i nie buforuje go, żeby
wysyłka dużych plików wideo/audio przechodziła bez zmian w backendzie.
Timeouty podniesione, bo panel trzyma WebSocket otwarty na stałe.
- Root Dockerfile wraca do samego Pythona i wystawia port 8000.
- compose.yaml uruchamia obie usługi we wspólnej sieci: panel na :80,
API na :8000 (wystawione do diagnostyki).
- Katalog static/ znika z repozytorium - nie ma już czego serwować.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Panel działa w osobnym kontenerze, więc serwis Pythona przestaje serwować
pliki statyczne:
- usunięty mount StaticFiles na "/" wraz z importem
- tytuł aplikacji zmieniony na "ŻyrarBOT API"
- GET / zwraca opis usługi zamiast panelu, żeby wejście pod adres backendu
nie kończyło się nic niemówiącym 404
- GET /api/health informuje, czy bot zdążył połączyć się z Discordem;
przydaje się jako healthcheck kontenera i przy diagnozie proxy
Pozostałe endpointy oraz WebSocket pozostają bez zmian.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
static/app.js (1196 linii imperatywnego JS operującego na getElementById)
oraz static/index.html zastąpione w całości aplikacją z frontend/.
Arkusz stylów został wcześniej przeniesiony, nie usuwamy go ponownie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Widok rozbity na komponenty odpowiadające kafelkom pulpitu:
- AppHeader, VoiceChannelPanel, PlayerPanel, AddTrackPanel
- UploadsPanel wraz z UploadedFileRow współdzielonym z oknem modalnym
- QueuePanel i QueueItem - przeciąganie, zmiana kolejności, limit odtworzeń
- HistoryPanel, ModalShell, SettingsModal, UploadsLibraryModal, ToastHost
Arkusz stylów przeniesiony ze static/style.css bez zmian w wyglądzie.
Style, które app.js wstrzykiwał inline przy budowaniu HTML-a, przeniesione
do klas współdzielonych i bloków scoped w komponentach.
Poprawki bezpieczeństwa wynikające z przepisania:
- Tytuły utworów i nazwy plików nie trafiają już do innerHTML. Poprzednia
wersja składała wiersze kolejki, historii i listy plików ze stringów,
więc tytuł zawierający znaczniki HTML był wstawiany do DOM bez escapowania.
- Nazwy plików nie są już wklejane do atrybutów onclick z ręcznym
escapowaniem apostrofu, które i tak nie obejmowało cudzysłowu. Zdarzenia
podpina Vue, bez generowania kodu w stringach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Logika, która w app.js żyła w zmiennych globalnych, trafia do czterech
composables o wyraźnym zakresie odpowiedzialności:
- useBotStatus jedno połączenie WebSocket na aplikację, z licznikiem
subskrybentów i ponawianiem po zerwaniu. Flaga paused
wstrzymuje aktualizacje na czas przeciągania utworów.
- useToasts kolejka powiadomień wraz z helperem run(), który
opakowuje wywołanie API w parę komunikatów sukces/błąd.
- useTrackProgress płynny pasek postępu - między pakietami z serwera czas
dopisujemy lokalnie, a każdy pakiet resynchronizuje licznik.
- useUploads serwerowa baza plików, współdzielona przez kafelek na
pulpicie i okno z pełną listą.
Dochodzi też utils/plural.ts z polską odmianą liczebników, zamiast
powtarzanego w kilku miejscach warunku count < 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- types/status.ts spisuje kształt odpowiedzi /api/status oraz pakietów
rozgłaszanych po WebSockecie. Dotąd struktura tych danych istniała
wyłącznie w głowie autora i w rozsianych po kodzie odwołaniach.
- api/client.ts zbiera wszystkie adresy endpointów w jednym miejscu i
ujednolica obsługę błędów: pole "detail" z odpowiedzi FastAPI trafia do
wyjątku ApiError, więc warstwa widoku dostaje gotowy komunikat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Panel przestaje być zestawem plików statycznych serwowanych przez backend
i staje się osobnym projektem w katalogu frontend/.
- Vue 3 z Composition API jako framework - szablony komponentów są bliskie
dotychczasowemu HTML-owi, więc migracja nie wymaga przepisywania widoków
na inny model mentalny.
- TypeScript w trybie strict; kontrakt danych z API będzie opisany typami.
- Vite jako bundler, z aliasem @ na src/ i budowaniem do dist/.
- Serwer deweloperski przekierowuje /api i /ws na backend; adres można
nadpisać zmienną VITE_BACKEND_URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>