update environment configuration for local development, enhance database initialization, and improve Nginx routing for admin access
This commit is contained in:
1 parent
2e061dff28
commit
1ae0ad121b
10 files changed
+183
-35
No files matched your search
+12
-11
@@ -15,13 +15,14 @@ CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|||||||
MEDIA_DOMAIN=media.linearvoid.tv
|
MEDIA_DOMAIN=media.linearvoid.tv
|
||||||
|
|
||||||
# Public frontend URLs (Vite reads these from the repository root via frontend/vite.config.ts)
|
# Public frontend URLs (Vite reads these from the repository root via frontend/vite.config.ts)
|
||||||
# Local defaults keep development on localhost. Production should use:
|
# 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_APP_URL=https://linearvoid.tv
|
||||||
# VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws
|
VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws
|
||||||
# VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8
|
VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8
|
||||||
VITE_PUBLIC_APP_URL=http://localhost:8080
|
# Optional direct-local fallback when bypassing the public proxy:
|
||||||
VITE_PUBLIC_WS_URL=ws://localhost:3000/ws
|
# VITE_PUBLIC_APP_URL=http://localhost
|
||||||
VITE_PUBLIC_HLS_URL=http://localhost:8080/hls/playlist.m3u8
|
# 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 Container Registry (for self-hosted Docker images)
|
||||||
GITEA_REGISTRY_URL=git.allucanget.biz
|
GITEA_REGISTRY_URL=git.allucanget.biz
|
||||||
@@ -29,13 +30,13 @@ GITEA_USERNAME=your-gitea-username
|
|||||||
GITEA_TOKEN=your-gitea-token
|
GITEA_TOKEN=your-gitea-token
|
||||||
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid
|
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid
|
||||||
|
|
||||||
# Chat / Application Database (PostgreSQL or SQLite for local)
|
# Chat / Application Database (compose-safe PostgreSQL by default, SQLite optional)
|
||||||
DATABASE_URL=postgres://user:password@localhost:5432/linearvoid
|
DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable
|
||||||
# Or for SQLite (local development):
|
# Or for SQLite-only local development:
|
||||||
# DATABASE_URL=sqlite://./data/chat.db
|
# DATABASE_URL=sqlite://./data/chat.db
|
||||||
|
|
||||||
# Redis (for presence, rate limiting, pub/sub)
|
# 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)
|
# Service Ports (for local docker-compose)
|
||||||
PLAYOUT_PORT=8080
|
PLAYOUT_PORT=8080
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# Change Log for linearvoid
|
# Change Log for linearvoid
|
||||||
|
|
||||||
|
<!-- markdownlint-disable MD024 -->
|
||||||
|
|
||||||
|
## [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
|
## [1.0.1] - 2026-08-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -54,9 +54,14 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
MEDIA_DOMAIN=media.linearvoid.tv
|
MEDIA_DOMAIN=media.linearvoid.tv
|
||||||
|
|
||||||
# Public frontend URLs
|
# Public frontend URLs
|
||||||
VITE_PUBLIC_APP_URL=http://localhost:8080
|
# Preferred local setup uses the real public domains through Cloudflare and Nginx Proxy Manager.
|
||||||
VITE_PUBLIC_WS_URL=ws://localhost:3000/ws
|
VITE_PUBLIC_APP_URL=https://linearvoid.tv
|
||||||
VITE_PUBLIC_HLS_URL=http://localhost:8080/hls/playlist.m3u8
|
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 Container Registry (for self-hosted Docker images)
|
||||||
GITEA_REGISTRY_URL=git.allucanget.biz
|
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
|
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid
|
||||||
|
|
||||||
# Chat / Database
|
# Chat / Database
|
||||||
DATABASE_URL=postgres://user:password@localhost:5432/linearvoid
|
DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable
|
||||||
# or for SQLite in local development:
|
# or for SQLite-only local development:
|
||||||
# DATABASE_URL=sqlite://./data/chat.db
|
# DATABASE_URL=sqlite://./data/chat.db
|
||||||
REDIS_URL=redis://localhost:6379/0
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
|
||||||
# Ports (for local development)
|
# Ports (for local development)
|
||||||
PLAYOUT_PORT=8080
|
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**:
|
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.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.
|
- `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:<password>@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:<password>@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`.
|
- 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**:
|
5. **Create Data Directories**:
|
||||||
@@ -182,9 +187,10 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Access the Application**:
|
2. **Access the Application**:
|
||||||
- Viewer: <http://localhost:8080>
|
- Preferred local access with the documented proxy setup: <https://linearvoid.tv>
|
||||||
- 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: <https://linearvoid.tv/admin>
|
||||||
- Admin Interface: <http://localhost:8080/admin> (use the credentials from `.env` or default admin/admin)
|
- Chat WebSocket: `wss://linearvoid.tv/ws`
|
||||||
|
- Direct-local fallback when bypassing Cloudflare/Nginx Proxy Manager: <http://localhost>, <http://localhost/admin>, and `ws://localhost/ws`
|
||||||
- Health Checks:
|
- Health Checks:
|
||||||
- Playout: <http://localhost:8081/health>
|
- Playout: <http://localhost:8081/health>
|
||||||
- Chat: <http://localhost:3000/health>
|
- Chat: <http://localhost:3000/health>
|
||||||
@@ -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.
|
- Keep `MEDIA_DOMAIN`, R2 credentials, and Gitea registry values consistent with the external infrastructure.
|
||||||
- Set `MEDIA_DOMAIN=media.linearvoid.tv`.
|
- 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`.
|
- 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**:
|
4. **Create runtime directories on the server**:
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -1,10 +1,11 @@
|
|||||||
module chat
|
module chat
|
||||||
|
|
||||||
go 1.23
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/redis/go-redis/v9 v9.7.0
|
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/driver/sqlite v1.5.7
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
@@ -12,7 +13,14 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -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=
|
||||||
+45
-11
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -56,11 +57,25 @@ type Presence struct {
|
|||||||
|
|
||||||
// initRedis initializes Redis for presence, rate limiting, and pub/sub (ADR-007)
|
// initRedis initializes Redis for presence, rate limiting, and pub/sub (ADR-007)
|
||||||
func initRedis() {
|
func initRedis() {
|
||||||
redisClient = redis.NewClient(&redis.Options{
|
redisAddress := os.Getenv("REDIS_URL")
|
||||||
Addr: os.Getenv("REDIS_URL"),
|
if redisAddress == "" {
|
||||||
Password: "",
|
redisAddress = "redis:6379"
|
||||||
DB: 0,
|
}
|
||||||
})
|
|
||||||
|
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 {
|
if err := redisClient.Ping(context.Background()).Err(); err != nil {
|
||||||
log.Printf("Redis connection warning: %v (rate limiting and presence will be disabled)", err)
|
log.Printf("Redis connection warning: %v (rate limiting and presence will be disabled)", err)
|
||||||
redisClient = nil
|
redisClient = nil
|
||||||
@@ -72,12 +87,31 @@ func initRedis() {
|
|||||||
// initDB initializes SQLite for message history and moderation audit log (ADR-006)
|
// initDB initializes SQLite for message history and moderation audit log (ADR-006)
|
||||||
func initDB() {
|
func initDB() {
|
||||||
var err error
|
var err error
|
||||||
|
databaseURL := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
||||||
|
driverName := "sqlite"
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case databaseURL == "":
|
||||||
db, err = gorm.Open(sqlite.Open("chat.db"), &gorm.Config{})
|
db, err = gorm.Open(sqlite.Open("chat.db"), &gorm.Config{})
|
||||||
if err != nil {
|
case strings.HasPrefix(databaseURL, "sqlite://"):
|
||||||
log.Fatal("Failed to connect to SQLite:", err)
|
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{})
|
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
|
// 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)
|
// Announce presence (ADR-007)
|
||||||
username := r.URL.Query().Get("username")
|
username := r.URL.Query().Get("username")
|
||||||
room := r.URL.Query().Get("room")
|
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)
|
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"})
|
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()
|
mutex.Lock()
|
||||||
delete(clients, conn)
|
delete(clients, conn)
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
if username != "" && room != "" {
|
if redisClient != nil && username != "" && room != "" {
|
||||||
redisClient.Del(context.Background(), fmt.Sprintf("presence:%s:%s", room, username))
|
redisClient.Del(context.Background(), fmt.Sprintf("presence:%s:%s", room, username))
|
||||||
broadcast(Message{Room: room, Username: "system", Text: username + " left " + room, Type: "system"})
|
broadcast(Message{Room: room, Username: "system", Text: username + " left " + room, Type: "system"})
|
||||||
}
|
}
|
||||||
@@ -230,6 +264,6 @@ func main() {
|
|||||||
port = "3000"
|
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))
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,12 @@ func LoadPlaylist(playlistPath string) ([]VideoFile, error) {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "file ") {
|
||||||
|
line = strings.TrimSpace(strings.TrimPrefix(line, "file "))
|
||||||
|
line = strings.Trim(line, "'\"")
|
||||||
|
}
|
||||||
|
|
||||||
videos = append(videos, VideoFile{Path: line})
|
videos = append(videos, VideoFile{Path: line})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,8 +183,9 @@ func main() {
|
|||||||
"-f", "hls",
|
"-f", "hls",
|
||||||
"-hls_time", "6",
|
"-hls_time", "6",
|
||||||
"-hls_list_size", "24",
|
"-hls_list_size", "24",
|
||||||
|
"-strftime", "1",
|
||||||
"-hls_flags", "delete_segments+append_list+independent_segments",
|
"-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")
|
"/app/hls/playlist.m3u8")
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ while true; do
|
|||||||
-f hls \
|
-f hls \
|
||||||
-hls_time "${HLS_SEGMENT_DURATION}" \
|
-hls_time "${HLS_SEGMENT_DURATION}" \
|
||||||
-hls_list_size "${HLS_WINDOW_SIZE}" \
|
-hls_list_size "${HLS_WINDOW_SIZE}" \
|
||||||
|
-strftime 1 \
|
||||||
-hls_flags delete_segments+append_list \
|
-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)..."
|
"/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..."
|
echo "Playout loop crashed or ended. Restarting in 5 seconds..."
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
envDir: "..",
|
envDir: "..",
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve(__dirname, "index.html"),
|
||||||
|
admin: resolve(__dirname, "admin/index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -3,6 +3,9 @@ events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
resolver 127.0.0.11 ipv6=off valid=30s;
|
resolver 127.0.0.11 ipv6=off valid=30s;
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
@@ -24,6 +27,18 @@ http {
|
|||||||
root /srv/frontend;
|
root /srv/frontend;
|
||||||
index index.html;
|
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
|
# Serve static frontend
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
Reference in new issue
Block a user