From 1ae0ad121b9785ac095f37043a9d039db253eb46 Mon Sep 17 00:00:00 2001 From: zwitschi Date: Mon, 3 Aug 2026 14:26:11 +0200 Subject: [PATCH] update environment configuration for local development, enhance database initialization, and improve Nginx routing for admin access --- .env.example | 23 +++++++------ CHANGELOG.md | 14 ++++++++ SETUP.md | 27 +++++++++------ backend/chat/go.mod | 12 +++++-- backend/chat/go.sum | 52 ++++++++++++++++++++++++++++ backend/chat/src/main.go | 58 +++++++++++++++++++++++++------- backend/playout/src/playlist.go | 9 ++++- backend/playout/start-playout.sh | 3 +- frontend/vite.config.ts | 9 +++++ infrastructure/nginx/nginx.conf | 15 +++++++++ 10 files changed, 185 insertions(+), 37 deletions(-) create mode 100644 backend/chat/go.sum diff --git a/.env.example b/.env.example index 72ef951..913807a 100644 --- a/.env.example +++ b/.env.example @@ -15,13 +15,14 @@ CLOUDFLARE_ACCOUNT_ID=your-account-id MEDIA_DOMAIN=media.linearvoid.tv # Public frontend URLs (Vite reads these from the repository root via frontend/vite.config.ts) -# Local defaults keep development on localhost. Production should use: -# VITE_PUBLIC_APP_URL=https://linearvoid.tv -# VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws -# VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8 -VITE_PUBLIC_APP_URL=http://localhost:8080 -VITE_PUBLIC_WS_URL=ws://localhost:3000/ws -VITE_PUBLIC_HLS_URL=http://localhost:8080/hls/playlist.m3u8 +# Preferred local setup in this repo uses the real public domains through Cloudflare/Nginx Proxy Manager. +VITE_PUBLIC_APP_URL=https://linearvoid.tv +VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws +VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8 +# Optional direct-local fallback when bypassing the public proxy: +# VITE_PUBLIC_APP_URL=http://localhost +# VITE_PUBLIC_WS_URL=ws://localhost/ws +# VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8 # Gitea Container Registry (for self-hosted Docker images) GITEA_REGISTRY_URL=git.allucanget.biz @@ -29,13 +30,13 @@ GITEA_USERNAME=your-gitea-username GITEA_TOKEN=your-gitea-token GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid -# Chat / Application Database (PostgreSQL or SQLite for local) -DATABASE_URL=postgres://user:password@localhost:5432/linearvoid -# Or for SQLite (local development): +# Chat / Application Database (compose-safe PostgreSQL by default, SQLite optional) +DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable +# Or for SQLite-only local development: # DATABASE_URL=sqlite://./data/chat.db # Redis (for presence, rate limiting, pub/sub) -REDIS_URL=redis://localhost:6379/0 +REDIS_URL=redis://redis:6379/0 # Service Ports (for local docker-compose) PLAYOUT_PORT=8080 diff --git a/CHANGELOG.md b/CHANGELOG.md index fc6b3f4..7900f26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log for linearvoid + + +## [1.0.2] - 2026-08-03 + +### Changed + +- Updated local environment documentation and examples to prefer real-domain local routing through `linearvoid.tv` and compose-safe internal service hostnames. + +### Fixed + +- Fixed admin production build output and nginx routing so `/admin/` serves its own entrypoint. +- Fixed static asset serving so hashed JS/CSS files return correct MIME types and stale asset URLs return `404` instead of SPA HTML. +- Fixed chat database configuration so `DATABASE_URL` is honored for compose-safe Postgres or SQLite local development. + ## [1.0.1] - 2026-08-03 ### Changed diff --git a/SETUP.md b/SETUP.md index 5ad67ac..435573b 100644 --- a/SETUP.md +++ b/SETUP.md @@ -54,9 +54,14 @@ This guide covers prerequisites, environment setup, running the application loca MEDIA_DOMAIN=media.linearvoid.tv # Public frontend URLs - VITE_PUBLIC_APP_URL=http://localhost:8080 - VITE_PUBLIC_WS_URL=ws://localhost:3000/ws - VITE_PUBLIC_HLS_URL=http://localhost:8080/hls/playlist.m3u8 + # Preferred local setup uses the real public domains through Cloudflare and Nginx Proxy Manager. + VITE_PUBLIC_APP_URL=https://linearvoid.tv + VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws + VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8 + # Optional direct-local fallback when bypassing the public proxy: + # VITE_PUBLIC_APP_URL=http://localhost + # VITE_PUBLIC_WS_URL=ws://localhost/ws + # VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8 # Gitea Container Registry (for self-hosted Docker images) GITEA_REGISTRY_URL=git.allucanget.biz @@ -65,10 +70,10 @@ This guide covers prerequisites, environment setup, running the application loca GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid # Chat / Database - DATABASE_URL=postgres://user:password@localhost:5432/linearvoid - # or for SQLite in local development: + DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable + # or for SQLite-only local development: # DATABASE_URL=sqlite://./data/chat.db - REDIS_URL=redis://localhost:6379/0 + REDIS_URL=redis://redis:6379/0 # Ports (for local development) PLAYOUT_PORT=8080 @@ -98,7 +103,7 @@ This guide covers prerequisites, environment setup, running the application loca 4. **Check local value overrides for Docker Compose**: - `docker-compose.yml` builds images locally and maps the frontend build output from `frontend/dist` into nginx. - `docker-compose.prod.yml` pulls prebuilt images from the Gitea registry. - - For production on the VPS, change hostnames in `.env` so containers talk to each other over the compose network, for example `REDIS_URL=redis://redis:6379/0` and `DATABASE_URL=postgres://linearvoid:@db:5432/linearvoid`. + - Internal container-to-container URLs should use compose hostnames, for example `REDIS_URL=redis://redis:6379/0` and `DATABASE_URL=postgres://linearvoid:@db:5432/linearvoid?sslmode=disable`. - For production public routing, set `VITE_PUBLIC_APP_URL=https://linearvoid.tv`, `VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws`, and `VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8`. 5. **Create Data Directories**: @@ -182,9 +187,10 @@ This guide covers prerequisites, environment setup, running the application loca ``` 2. **Access the Application**: - - Viewer: - - Chat WebSocket: `ws://localhost:3000/ws` when using the local env defaults, or `ws://localhost:8080/ws` if you switch to same-origin local routing. - - Admin Interface: (use the credentials from `.env` or default admin/admin) + - Preferred local access with the documented proxy setup: + - Admin Interface: + - Chat WebSocket: `wss://linearvoid.tv/ws` + - Direct-local fallback when bypassing Cloudflare/Nginx Proxy Manager: , , and `ws://localhost/ws` - Health Checks: - Playout: - Chat: @@ -239,6 +245,7 @@ This guide covers prerequisites, environment setup, running the application loca - Keep `MEDIA_DOMAIN`, R2 credentials, and Gitea registry values consistent with the external infrastructure. - Set `MEDIA_DOMAIN=media.linearvoid.tv`. - Set `VITE_PUBLIC_APP_URL=https://linearvoid.tv`, `VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws`, and `VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8`. + - If you intentionally bypass the public proxy for local-only testing, switch those values to `http://localhost`, `ws://localhost/ws`, and `http://localhost/hls/playlist.m3u8`. 4. **Create runtime directories on the server**: diff --git a/backend/chat/go.mod b/backend/chat/go.mod index 1480796..5284f9d 100644 --- a/backend/chat/go.mod +++ b/backend/chat/go.mod @@ -1,10 +1,11 @@ module chat -go 1.23 +go 1.23.0 require ( github.com/gorilla/websocket v1.5.3 github.com/redis/go-redis/v9 v9.7.0 + gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 ) @@ -12,7 +13,14 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.5 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect -) \ No newline at end of file + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/text v0.24.0 // indirect +) diff --git a/backend/chat/go.sum b/backend/chat/go.sum new file mode 100644 index 0000000..93e3f3d --- /dev/null +++ b/backend/chat/go.sum @@ -0,0 +1,52 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/backend/chat/src/main.go b/backend/chat/src/main.go index 1d7f7f9..c75de02 100644 --- a/backend/chat/src/main.go +++ b/backend/chat/src/main.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/websocket" "github.com/redis/go-redis/v9" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -56,11 +57,25 @@ type Presence struct { // initRedis initializes Redis for presence, rate limiting, and pub/sub (ADR-007) func initRedis() { - redisClient = redis.NewClient(&redis.Options{ - Addr: os.Getenv("REDIS_URL"), - Password: "", - DB: 0, - }) + redisAddress := os.Getenv("REDIS_URL") + if redisAddress == "" { + redisAddress = "redis:6379" + } + + var options *redis.Options + var err error + if strings.Contains(redisAddress, "://") { + options, err = redis.ParseURL(redisAddress) + if err != nil { + log.Printf("Redis configuration warning: %v (rate limiting and presence will be disabled)", err) + redisClient = nil + return + } + } else { + options = &redis.Options{Addr: redisAddress} + } + + redisClient = redis.NewClient(options) if err := redisClient.Ping(context.Background()).Err(); err != nil { log.Printf("Redis connection warning: %v (rate limiting and presence will be disabled)", err) redisClient = nil @@ -72,12 +87,31 @@ func initRedis() { // initDB initializes SQLite for message history and moderation audit log (ADR-006) func initDB() { var err error - db, err = gorm.Open(sqlite.Open("chat.db"), &gorm.Config{}) - if err != nil { - log.Fatal("Failed to connect to SQLite:", err) + databaseURL := strings.TrimSpace(os.Getenv("DATABASE_URL")) + driverName := "sqlite" + + switch { + case databaseURL == "": + db, err = gorm.Open(sqlite.Open("chat.db"), &gorm.Config{}) + case strings.HasPrefix(databaseURL, "sqlite://"): + sqlitePath := strings.TrimPrefix(databaseURL, "sqlite://") + if sqlitePath == "" { + sqlitePath = "chat.db" + } + db, err = gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{}) + case strings.HasPrefix(databaseURL, "postgres://") || strings.HasPrefix(databaseURL, "postgresql://"): + driverName = "postgres" + db, err = gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) + default: + log.Fatalf("Unsupported DATABASE_URL scheme: %s", databaseURL) } + + if err != nil { + log.Fatalf("Failed to connect to %s database: %v", driverName, err) + } + db.AutoMigrate(&Message{}, &ModerationLog{}) - log.Println("SQLite database initialized (chat history and moderation log)") + log.Printf("%s database initialized (chat history and moderation log)", strings.Title(driverName)) } // broadcast sends a message to all connected clients in a room and saves to DB @@ -110,7 +144,7 @@ func handleConnection(w http.ResponseWriter, r *http.Request) { // Announce presence (ADR-007) username := r.URL.Query().Get("username") room := r.URL.Query().Get("room") - if username != "" && room != "" { + if redisClient != nil && username != "" && room != "" { redisClient.Set(context.Background(), fmt.Sprintf("presence:%s:%s", room, username), "online", 60*time.Second) broadcast(Message{Room: room, Username: "system", Text: username + " joined " + room, Type: "system"}) } @@ -119,7 +153,7 @@ func handleConnection(w http.ResponseWriter, r *http.Request) { mutex.Lock() delete(clients, conn) mutex.Unlock() - if username != "" && room != "" { + if redisClient != nil && username != "" && room != "" { redisClient.Del(context.Background(), fmt.Sprintf("presence:%s:%s", room, username)) broadcast(Message{Room: room, Username: "system", Text: username + " left " + room, Type: "system"}) } @@ -230,6 +264,6 @@ func main() { port = "3000" } - log.Printf("Chat Backend starting on :%s (WebSocket at /ws, health at /health, SQLite persistence enabled)", port) + log.Printf("Chat Backend starting on :%s (WebSocket at /ws, health at /health, database persistence enabled)", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } \ No newline at end of file diff --git a/backend/playout/src/playlist.go b/backend/playout/src/playlist.go index 1844ae4..65e4061 100644 --- a/backend/playout/src/playlist.go +++ b/backend/playout/src/playlist.go @@ -64,6 +64,12 @@ func LoadPlaylist(playlistPath string) ([]VideoFile, error) { if line == "" || strings.HasPrefix(line, "#") { continue } + + if strings.HasPrefix(line, "file ") { + line = strings.TrimSpace(strings.TrimPrefix(line, "file ")) + line = strings.Trim(line, "'\"") + } + videos = append(videos, VideoFile{Path: line}) } @@ -177,8 +183,9 @@ func main() { "-f", "hls", "-hls_time", "6", "-hls_list_size", "24", + "-strftime", "1", "-hls_flags", "delete_segments+append_list+independent_segments", - "-hls_segment_filename", "/app/hls/segment_%Y%m%d_%H%M%S_%%03d.ts", // unique immutable names per ADR-001 + "-hls_segment_filename", "/app/hls/segment_%Y%m%d_%H%M%S.ts", "/app/hls/playlist.m3u8") if err := cmd.Run(); err != nil { diff --git a/backend/playout/start-playout.sh b/backend/playout/start-playout.sh index a89bb5f..9ec1f09 100644 --- a/backend/playout/start-playout.sh +++ b/backend/playout/start-playout.sh @@ -34,8 +34,9 @@ while true; do -f hls \ -hls_time "${HLS_SEGMENT_DURATION}" \ -hls_list_size "${HLS_WINDOW_SIZE}" \ + -strftime 1 \ -hls_flags delete_segments+append_list \ - -hls_segment_filename "/app/hls/segment_%03d.ts" \ + -hls_segment_filename "/app/hls/segment_%Y%m%d_%H%M%S.ts" \ "/app/hls/playlist.m3u8" || echo "FFmpeg exited with error (will restart in 5s per ADR-001 resilience requirements)..." echo "Playout loop crashed or ended. Restarting in 5 seconds..." diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9193006..f1c26f5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,5 +1,14 @@ +import { resolve } from "node:path"; import { defineConfig } from "vite"; export default defineConfig({ envDir: "..", + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + admin: resolve(__dirname, "admin/index.html"), + }, + }, + }, }); diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf index 2902a9e..94328ab 100644 --- a/infrastructure/nginx/nginx.conf +++ b/infrastructure/nginx/nginx.conf @@ -3,6 +3,9 @@ events { } http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + resolver 127.0.0.11 ipv6=off valid=30s; map $http_upgrade $connection_upgrade { @@ -24,6 +27,18 @@ http { root /srv/frontend; index index.html; + location = /admin { + return 301 /admin/; + } + + location /admin/ { + try_files $uri $uri/ /admin/index.html; + } + + location /assets/ { + try_files $uri =404; + } + # Serve static frontend location / { try_files $uri $uri/ /index.html;