refactor: consolidate frontend serving and add health check
Backend / Lint i formatowanie (pull_request) Successful in 4s
Panel / Lint, typy i build (pull_request) Successful in 4m50s
Panel / Testowy build obrazu (pull_request) Failing after 2s
Backend / Testy i kontrakt API (pull_request) Successful in 9m24s
Backend / Testowy build obrazu (pull_request) Successful in 1m38s

This commit is contained in:
mmacierzanka committed 2026-08-21 15:29:45 +02:00
1 parent 240120db92
commit 384770565a
13 files changed
+42 -121

No files matched your search

-3
View File
@@ -1,6 +1,3 @@
# Kontekst obrazu backendu - panel ma własny Dockerfile w frontend/.
frontend/
__pycache__/
*.pyc
.git/
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
- name: Kontener odpowiada na API
run: |
docker run -d --name zyrarbot-ci -e DISCORD_TOKEN=ci-dummy zyrarbot-api:ci
probe="import json,urllib.request;json.load(urllib.request.urlopen('http://localhost:8000/api/status',timeout=2))"
probe="import json,urllib.request;json.load(urllib.request.urlopen('http://localhost:8000/api/health',timeout=2))"
for i in $(seq 1 30); do
if docker exec zyrarbot-ci python -c "$probe" 2>/dev/null; then
echo "API odpowiedziało po ${i}s"
+1 -1
View File
@@ -9,6 +9,6 @@ uploads/
# Cache narzędzi deweloperskich
.ruff_cache/
# Panel budowany jest z frontend/ i trafia do własnego obrazu.
# Zależności i artefakty buildu frontendu.
node_modules/
frontend/dist/
+14 -1
View File
@@ -1,3 +1,15 @@
# --- Etap 1: budowa panelu ---------------------------------------------------
FROM node:22-alpine AS frontend-build
WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# --- Etap 2: aplikacja -------------------------------------------------------
FROM python:3.13-slim
# Zależności systemowe: opus dla głosu, ffmpeg do dekodowania audio
@@ -15,8 +27,9 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
COPY --from=frontend-build /frontend/dist /app/static
EXPOSE 8000
# Serwis wystawia wyłącznie API i steruje botem - panel ma własny obraz.
# Python serwuje API oraz zbudowany panel webowy.
CMD ["python", "-u", "main.py"]
+2 -1
View File
@@ -17,7 +17,8 @@ Wymagane zmienne środowiskowe (plik `.env`):
| `DISCORD_TOKEN` | Token bota Discord. |
| `GUILD_ID` | Opcjonalnie - synchronizuje komendy tylko z jednym serwerem. |
Panel działa pod `http://localhost:8000`. W Dockerze: `docker compose up --build`.
Panel działa pod `http://localhost:8000` podczas uruchomienia lokalnego. W Dockerze
jest dostępny pod `http://localhost` po wykonaniu `docker compose up --build`.
## Testy
+3 -25
View File
@@ -1,10 +1,10 @@
services:
# Bot Discorda wraz z API sterującym. Nie serwuje żadnych plików panelu.
# Bot Discorda wraz z API i panelem webowym serwowanym przez FastAPI.
discord-bot:
build:
context: .
dockerfile: Dockerfile
container_name: zyrarbot-api
container_name: zyrarbot
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN}
- GUILD_ID=${GUILD_ID:-}
@@ -12,28 +12,6 @@ services:
volumes:
- ./config:/app/config
- ./uploads:/app/uploads
# Port wystawiony do debugowania API; panel łączy się siecią wewnętrzną.
ports:
- "8000:8000"
networks:
- zyrarbot
- "80:8000"
restart: unless-stopped
# Panel webowy: statyczny build Vue serwowany przez nginx, który przekazuje
# /api i /ws do kontenera bota - dzięki temu przeglądarka widzi jedno źródło.
panel:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: zyrarbot-panel
ports:
- "80:80"
depends_on:
- discord-bot
networks:
- zyrarbot
restart: unless-stopped
networks:
zyrarbot:
driver: bridge
-22
View File
@@ -1,22 +0,0 @@
# --- Etap 1: budowa panelu ---------------------------------------------------
FROM node:22-alpine AS build
WORKDIR /app
# Zależności instalujemy przed skopiowaniem źródeł, aby korzystać z cache warstw.
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# --- Etap 2: serwowanie przez nginx ------------------------------------------
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1
+7 -12
View File
@@ -2,8 +2,8 @@
Frontend napisany w **Vue 3 + TypeScript**, budowany przez **Vite**.
Panel jest **samodzielną aplikacją we własnym kontenerze**. Backend wystawia
wyłącznie API i steruje botem - nie serwuje żadnych plików statycznych.
Panel jest budowany jako statyczny frontend i w produkcji serwuje go Pythonowy
backend razem z API.
## Praca lokalna
@@ -13,8 +13,8 @@ npm run dev # http://localhost:5173, proxy /api i /ws na backend :8000
```
Backend uruchamiamy równolegle (`python main.py`). Serwer deweloperski Vite
przejmuje wtedy rolę nginxa i przekierowuje REST oraz WebSocket. Inny adres
backendu wskazuje zmienna `VITE_BACKEND_URL`:
przekierowuje REST oraz WebSocket do backendu. Inny adres backendu wskazuje
zmienna `VITE_BACKEND_URL`:
```bash
VITE_BACKEND_URL=http://192.168.1.10:8000 npm run dev
@@ -29,18 +29,13 @@ npm run smoke # test dymny zbudowanego artefaktu
## Kontener
`Dockerfile` buduje panel w etapie `node:22-alpine`, a wynik serwuje
`nginx:1.27-alpine`. Konfiguracja z `nginx.conf` przekazuje `/api` i `/ws`
do usługi `discord-bot`, dzięki czemu przeglądarka widzi jedno źródło
i nie potrzeba CORS-u.
Główny `Dockerfile` buduje panel w etapie `node:22-alpine`, a wynik kopiuje
do obrazu Pythona. FastAPI serwuje `/`, `/api` i `/ws` z jednego kontenera.
```bash
docker compose up --build # panel na :80, API na :8000
docker compose up --build # panel i API na :80
```
Aby panel łączył się z backendem pod innym adresem, wystarczy podmienić
`set $backend` w `nginx.conf` - obraz nie wymaga przebudowania frontendu.
## Struktura
```
-51
View File
@@ -1,51 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Nazwa usługi backendu z compose.yaml; zmienna wymusza rozwiązywanie DNS
# w czasie żądania, więc nginx wstaje nawet gdy bot jeszcze się nie podniósł.
resolver 127.0.0.11 valid=10s ipv6=off;
set $backend "http://discord-bot:8000";
# Aplikacja jednostronicowa - nieznane ścieżki oddajemy index.html.
location / {
try_files $uri $uri/ /index.html;
}
# Artefakty Vite mają skrót treści w nazwie, więc można je cache'ować na stałe.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location /api/ {
proxy_pass $backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Wysyłanie plików wideo/audio na serwer - bez limitu rozmiaru.
client_max_body_size 0;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /ws {
proxy_pass $backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# Panel trzyma połączenie otwarte i odbiera stan raz na sekundę.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
+2 -3
View File
@@ -2,8 +2,7 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// Panel jest samodzielną aplikacją - buduje się do dist/ i trafia do własnego
// obrazu z nginxem. Backend nie serwuje żadnych plików statycznych.
// Panel buduje się do dist/ i trafia do obrazu aplikacji serwowanej przez Pythona.
const BACKEND = process.env.VITE_BACKEND_URL ?? 'http://localhost:8000'
export default defineConfig({
@@ -15,7 +14,7 @@ export default defineConfig({
},
server: {
port: 5173,
// W trybie deweloperskim rolę nginxa przejmuje proxy serwera Vite.
// W trybie deweloperskim proxy Vite kieruje API do backendu.
proxy: {
'/api': BACKEND,
'/ws': { target: BACKEND.replace(/^http/, 'ws'), ws: true },
+1
View File
@@ -10,6 +10,7 @@ from zyrarbot.settings import Settings
EXPECTED_ENDPOINTS = {
("get", "/api/status"),
("get", "/api/health"),
("post", "/api/play"),
("post", "/api/play_file"),
("post", "/api/pause"),
+4
View File
@@ -55,6 +55,10 @@ class MusicPlayer:
self._states[guild_id] = GuildMusicState(guild_id=guild_id, volume=1.0)
return self._states[guild_id]
@property
def is_ready(self) -> bool:
return self._bot.is_ready()
@property
def default_plays(self) -> int:
return self._config.get_default_plays()
+7 -1
View File
@@ -4,7 +4,8 @@ from typing import Any
from fastapi import APIRouter, Depends
from zyrarbot.ui.api.dependencies import get_status_service
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.ui.api.dependencies import get_player, get_status_service
from zyrarbot.ui.api.status_service import StatusService
router = APIRouter(prefix="/api", tags=["status"])
@@ -13,3 +14,8 @@ router = APIRouter(prefix="/api", tags=["status"])
@router.get("/status")
async def get_status(status: StatusService = Depends(get_status_service)) -> dict[str, Any]:
return status.snapshot()
@router.get("/health")
async def get_health(player: MusicPlayer = Depends(get_player)) -> dict[str, Any]:
return {"status": "ok", "ready": player.is_ready}