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
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:
13 files changed
+42
-121
No files matched your search
@@ -1,6 +1,3 @@
|
||||
# Kontekst obrazu backendu - panel ma własny Dockerfile w frontend/.
|
||||
frontend/
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.git/
|
||||
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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,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 },
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
Reference in new issue
Block a user