diff --git a/backend/src/controllers/AdminController.cpp b/backend/src/controllers/AdminController.cpp index 9c4fb95..e832463 100644 --- a/backend/src/controllers/AdminController.cpp +++ b/backend/src/controllers/AdminController.cpp @@ -2956,13 +2956,10 @@ void AdminController::getSSLSettings(const HttpRequestPtr &req, *dbClient << "SELECT domain, acme_email, certificate_status, certificate_expiry, " "last_renewal_attempt, last_renewal_error, auto_renewal_enabled, updated_at " "FROM ssl_settings WHERE id = 1" - >> [callback, dbClient](const Result& r) { + >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; - std::string dbStatus = "none"; - std::string domain = ""; - if (r.empty()) { // Return defaults if no settings exist resp["domain"] = ""; @@ -2974,12 +2971,9 @@ void AdminController::getSSLSettings(const HttpRequestPtr &req, resp["autoRenewalEnabled"] = true; } else { const auto& row = r[0]; - domain = row["domain"].isNull() ? "" : row["domain"].as(); - dbStatus = row["certificate_status"].as(); - - resp["domain"] = domain; + resp["domain"] = row["domain"].isNull() ? "" : row["domain"].as(); resp["acmeEmail"] = row["acme_email"].isNull() ? "" : row["acme_email"].as(); - resp["certificateStatus"] = dbStatus; + resp["certificateStatus"] = row["certificate_status"].as(); if (!row["certificate_expiry"].isNull()) { resp["certificateExpiry"] = row["certificate_expiry"].as(); @@ -3002,50 +2996,6 @@ void AdminController::getSSLSettings(const HttpRequestPtr &req, resp["autoRenewalEnabled"] = row["auto_renewal_enabled"].as(); } - // Check for existing certificates on disk if DB shows "none" - // This handles the case where certbot ran during cloud-init - if (dbStatus == "none") { - try { - // Check /etc/letsencrypt/live directory for any certificates - std::string letsencryptDir = "/etc/letsencrypt/live"; - if (std::filesystem::exists(letsencryptDir) && std::filesystem::is_directory(letsencryptDir)) { - for (const auto& entry : std::filesystem::directory_iterator(letsencryptDir)) { - if (entry.is_directory()) { - std::string certPath = entry.path().string() + "/fullchain.pem"; - if (std::filesystem::exists(certPath)) { - std::string detectedDomain = entry.path().filename().string(); - LOG_INFO << "Detected existing SSL certificate for: " << detectedDomain; - - // Update response to show active certificate - resp["certificateStatus"] = "active"; - if (resp["domain"].asString().empty()) { - resp["domain"] = detectedDomain; - } - - // Update database to reflect the existing certificate - *dbClient << "INSERT INTO ssl_settings (id, domain, certificate_status, updated_at) " - "VALUES (1, $1, 'active', CURRENT_TIMESTAMP) " - "ON CONFLICT (id) DO UPDATE SET " - "certificate_status = 'active', " - "domain = CASE WHEN ssl_settings.domain = '' THEN $1 ELSE ssl_settings.domain END, " - "updated_at = CURRENT_TIMESTAMP" - << detectedDomain - >> [](const Result&) { - LOG_INFO << "Updated ssl_settings with detected certificate"; - } - >> [](const DrogonDbException& e) { - LOG_ERROR << "Failed to update ssl_settings: " << e.base().what(); - }; - break; - } - } - } - } - } catch (const std::exception& e) { - LOG_DEBUG << "Error checking for certificates: " << e.what(); - } - } - callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { diff --git a/devops/terraform/main.tf b/devops/terraform/main.tf index c9c4373..caf7931 100644 --- a/devops/terraform/main.tf +++ b/devops/terraform/main.tf @@ -73,6 +73,7 @@ module "forgejo" { ssh_keys = module.ssh_keys.forgejo_ssh_key_ids droplet_size = var.forgejo_droplet_size droplet_image = var.forgejo_droplet_image + volume_size = var.forgejo_volume_size ssh_port = var.forgejo_ssh_port git_ssh_port = var.forgejo_git_ssh_port domain = var.forgejo_domain diff --git a/devops/terraform/modules/forgejo/cloud-init.yaml.tpl b/devops/terraform/modules/forgejo/cloud-init.yaml.tpl index 392655a..a480d8f 100644 --- a/devops/terraform/modules/forgejo/cloud-init.yaml.tpl +++ b/devops/terraform/modules/forgejo/cloud-init.yaml.tpl @@ -152,6 +152,54 @@ write_files: echo "Firewall configured successfully" permissions: '0755' + # Volume mount script + - path: /usr/local/bin/mount-volume.sh + content: | + #!/bin/bash + set -e + + VOLUME_NAME="${volume_name}" + MOUNT_POINT="/mnt/forgejo" + + # Create mount point + mkdir -p "$MOUNT_POINT" + + # Find the volume device + # DigitalOcean volumes are typically at /dev/disk/by-id/scsi-0DO_Volume_* + VOLUME_DEV=$(readlink -f /dev/disk/by-id/scsi-0DO_Volume_$VOLUME_NAME 2>/dev/null || true) + + if [ -z "$VOLUME_DEV" ]; then + echo "Waiting for volume to attach..." + sleep 10 + VOLUME_DEV=$(readlink -f /dev/disk/by-id/scsi-0DO_Volume_$VOLUME_NAME 2>/dev/null || true) + fi + + if [ -n "$VOLUME_DEV" ]; then + # Check if already mounted + if ! mountpoint -q "$MOUNT_POINT"; then + mount "$VOLUME_DEV" "$MOUNT_POINT" + echo "Volume mounted at $MOUNT_POINT" + fi + + # Add to fstab if not already there + if ! grep -q "$VOLUME_DEV" /etc/fstab; then + echo "$VOLUME_DEV $MOUNT_POINT ext4 defaults,nofail,discard 0 2" >> /etc/fstab + fi + + # Create subdirectories + mkdir -p "$MOUNT_POINT/forgejo-data" + mkdir -p "$MOUNT_POINT/forgejo-db" + mkdir -p "$MOUNT_POINT/runner-data" + + # Set permissions (UID 1000 is typically the forgejo user in container) + chown -R 1000:1000 "$MOUNT_POINT/forgejo-data" + chown -R 999:999 "$MOUNT_POINT/forgejo-db" # postgres user + chown -R 1000:1000 "$MOUNT_POINT/runner-data" + else + echo "WARNING: Volume not found. Please attach volume manually." + fi + permissions: '0755' + # Swap configuration script (needed for 1GB RAM) - path: /usr/local/bin/configure-swap.sh content: | @@ -221,7 +269,7 @@ write_files: POSTGRES_PASSWORD: $${POSTGRES_PASSWORD} POSTGRES_DB: $${POSTGRES_DB:-forgejo} volumes: - - /var/lib/forgejo/forgejo-db:/var/lib/postgresql/data + - /mnt/forgejo/forgejo-db:/var/lib/postgresql/data networks: - forgejo-internal healthcheck: @@ -278,7 +326,7 @@ write_files: FORGEJO__log__MODE: "console" FORGEJO__log__LEVEL: "Info" volumes: - - /var/lib/forgejo/forgejo-data:/data + - /mnt/forgejo/forgejo-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro networks: @@ -346,7 +394,7 @@ write_files: DOCKER_TLS_VERIFY: "1" DOCKER_CERT_PATH: /certs/client volumes: - - /var/lib/forgejo/runner-data:/data + - /mnt/forgejo/runner-data:/data - dind-certs-client:/certs/client:ro networks: - forgejo-internal @@ -523,7 +571,7 @@ write_files: | Domain: ${domain} | Git SSH Port: ${git_ssh_port} | | - | Data location: /var/lib/forgejo | + | Data location: /mnt/forgejo | | Docker compose: /opt/forgejo | | | | Commands: | @@ -585,10 +633,12 @@ runcmd: # Configure swap (important for 1GB RAM) - /usr/local/bin/configure-swap.sh - # Create data directories on local disk - - mkdir -p /var/lib/forgejo/forgejo-data /var/lib/forgejo/forgejo-db /var/lib/forgejo/runner-data - - chown -R 1000:1000 /var/lib/forgejo/forgejo-data /var/lib/forgejo/runner-data - - chown -R 999:999 /var/lib/forgejo/forgejo-db + # Mount the volume + - /usr/local/bin/mount-volume.sh + + # Fix ownership for Forgejo container (runs as UID 1000) + # This must run AFTER all directories are created to ensure correct permissions + - chown -R 1000:1000 /mnt/forgejo/forgejo-data # Configure firewall - /usr/local/bin/configure-firewall.sh diff --git a/devops/terraform/modules/forgejo/main.tf b/devops/terraform/modules/forgejo/main.tf index 9b7ddcc..d05ec67 100644 --- a/devops/terraform/modules/forgejo/main.tf +++ b/devops/terraform/modules/forgejo/main.tf @@ -22,6 +22,19 @@ resource "random_password" "forgejo_jwt_secret" { special = false } +# ============================================================================= +# Forgejo Volume (Block Storage) +# ============================================================================= + +resource "digitalocean_volume" "forgejo" { + name = "${var.project_name}-forgejo-${var.environment}" + region = var.region + size = var.volume_size + initial_filesystem_type = "ext4" + description = "Forgejo data volume for ${var.project_name}" + tags = var.tags +} + # ============================================================================= # Forgejo Droplet # ============================================================================= @@ -44,6 +57,7 @@ resource "digitalocean_droplet" "forgejo" { ssh_port = var.ssh_port git_ssh_port = var.git_ssh_port vpc_ip_range = var.vpc_ip_range + volume_name = "${var.project_name}-forgejo-${var.environment}" domain = var.domain postgres_password = random_password.postgres.result forgejo_secret_key = random_password.forgejo_secret_key.result @@ -59,6 +73,15 @@ resource "digitalocean_droplet" "forgejo" { } } +# ============================================================================= +# Volume Attachment +# ============================================================================= + +resource "digitalocean_volume_attachment" "forgejo" { + droplet_id = digitalocean_droplet.forgejo.id + volume_id = digitalocean_volume.forgejo.id +} + # ============================================================================= # DNS Record (optional - requires domain to be managed by DigitalOcean) # ============================================================================= diff --git a/devops/terraform/modules/forgejo/outputs.tf b/devops/terraform/modules/forgejo/outputs.tf index 70a03d3..6a39f37 100644 --- a/devops/terraform/modules/forgejo/outputs.tf +++ b/devops/terraform/modules/forgejo/outputs.tf @@ -18,6 +18,16 @@ output "urn" { value = digitalocean_droplet.forgejo.urn } +output "volume_id" { + description = "ID of the Forgejo volume" + value = digitalocean_volume.forgejo.id +} + +output "volume_name" { + description = "Name of the Forgejo volume" + value = digitalocean_volume.forgejo.name +} + output "dns_record_fqdn" { description = "FQDN of the DNS record (if managed)" value = var.manage_dns ? digitalocean_record.forgejo[0].fqdn : null diff --git a/devops/terraform/modules/forgejo/variables.tf b/devops/terraform/modules/forgejo/variables.tf index d047983..ffee337 100644 --- a/devops/terraform/modules/forgejo/variables.tf +++ b/devops/terraform/modules/forgejo/variables.tf @@ -40,6 +40,12 @@ variable "droplet_image" { default = "debian-12-x64" } +variable "volume_size" { + description = "Size of the data volume in GB" + type = number + default = 50 +} + variable "ssh_port" { description = "System SSH port (non-standard, VPC only)" type = number diff --git a/devops/terraform/outputs.tf b/devops/terraform/outputs.tf index b6be108..e5fb3c1 100644 --- a/devops/terraform/outputs.tf +++ b/devops/terraform/outputs.tf @@ -55,6 +55,11 @@ output "forgejo_private_ip" { value = module.forgejo.private_ip } +output "forgejo_volume_id" { + description = "ID of the Forgejo volume" + value = module.forgejo.volume_id +} + output "forgejo_ssh_port" { description = "System SSH port for Forgejo (VPC only)" value = var.forgejo_ssh_port diff --git a/devops/terraform/variables.tf b/devops/terraform/variables.tf index 2e337e1..67b9884 100644 --- a/devops/terraform/variables.tf +++ b/devops/terraform/variables.tf @@ -105,6 +105,12 @@ variable "forgejo_droplet_image" { default = "debian-12-x64" } +variable "forgejo_volume_size" { + description = "Size of the Forgejo data volume in GB" + type = number + default = 50 +} + variable "forgejo_domain" { description = "Domain name for Forgejo (e.g., qbit.realms.pub)" type = string diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index abdfbfc..5bbaf84 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -81,7 +81,6 @@ services: volumes: - ./config.json:/app/config.json - uploads:/app/uploads - - /etc/letsencrypt:/etc/letsencrypt:ro healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] interval: 10s diff --git a/frontend/src/lib/components/StreamPlayer.svelte b/frontend/src/lib/components/StreamPlayer.svelte index 754df2b..60ca496 100644 --- a/frontend/src/lib/components/StreamPlayer.svelte +++ b/frontend/src/lib/components/StreamPlayer.svelte @@ -12,17 +12,6 @@ const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088'; - // Helper for dynamic host detection - function getStreamHost() { - if (!browser) return 'localhost'; - return window.location.hostname; - } - - function getStreamProtocol() { - if (!browser) return 'http'; - return window.location.protocol === 'https:' ? 'https' : 'http'; - } - let player; let playerElement; let viewerToken = null; @@ -122,12 +111,10 @@ function initializePlayer() { if (!playerElement || !window.OvenPlayer || !viewerToken || !actualStreamKey) return; - const host = getStreamHost(); - const proto = getStreamProtocol(); const sources = [ { type: 'hls', - file: `${proto}://${host}:${STREAM_PORT}/app/${actualStreamKey}/llhls.m3u8?token=${viewerToken}`, + file: `http://localhost:${STREAM_PORT}/app/${actualStreamKey}/llhls.m3u8?token=${viewerToken}`, label: 'LLHLS' } ]; diff --git a/frontend/src/lib/components/StreamTileOverlay.svelte b/frontend/src/lib/components/StreamTileOverlay.svelte index 089e47f..cc4d0d6 100644 --- a/frontend/src/lib/components/StreamTileOverlay.svelte +++ b/frontend/src/lib/components/StreamTileOverlay.svelte @@ -6,17 +6,6 @@ const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088'; - // Helper for dynamic host detection - function getStreamHost() { - if (!browser) return 'localhost'; - return window.location.hostname; - } - - function getStreamProtocol() { - if (!browser) return 'http'; - return window.location.protocol === 'https:' ? 'https' : 'http'; - } - let players = {}; let viewerTokens = {}; let offlineStreams = {}; // Track which streams are offline @@ -138,12 +127,10 @@ const isMuted = $streamTiles.unmutedStream !== stream.streamKey; - const host = getStreamHost(); - const proto = getStreamProtocol(); const sources = [ { type: 'hls', - file: `${proto}://${host}:${STREAM_PORT}/app/${stream.streamKey}/llhls.m3u8?token=${token}`, + file: `http://localhost:${STREAM_PORT}/app/${stream.streamKey}/llhls.m3u8?token=${token}`, label: 'LLHLS' } ]; diff --git a/frontend/src/lib/components/UbercoinTipModal.svelte b/frontend/src/lib/components/UbercoinTipModal.svelte index 49b30f0..e80b4a7 100644 --- a/frontend/src/lib/components/UbercoinTipModal.svelte +++ b/frontend/src/lib/components/UbercoinTipModal.svelte @@ -407,7 +407,7 @@
Ü - Your balance: {formatUbercoin($ubercoinBalance)} + Balance: {formatUbercoin($ubercoinBalance)}
diff --git a/frontend/src/lib/stores/nakama.js b/frontend/src/lib/stores/nakama.js index 2bda4d6..a6963cb 100644 --- a/frontend/src/lib/stores/nakama.js +++ b/frontend/src/lib/stores/nakama.js @@ -1,33 +1,11 @@ import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; -// Nakama configuration - dynamically detect from browser location for production +// Nakama configuration from environment const NAKAMA_SERVER_KEY = import.meta.env.VITE_NAKAMA_SERVER_KEY || 'defaultkey'; - -// Dynamically detect host/protocol from browser location -// This ensures production uses the correct domain and SSL settings -function getNakamaConfig() { - if (!browser) { - // SSR fallback - use env vars or defaults - return { - host: import.meta.env.VITE_NAKAMA_HOST || 'localhost', - port: import.meta.env.VITE_NAKAMA_PORT || '80', - useSSL: import.meta.env.VITE_NAKAMA_USE_SSL === 'true' - }; - } - // Browser: use current page's host/protocol - const isSSL = window.location.protocol === 'https:'; - return { - host: window.location.hostname, - port: isSSL ? '443' : (window.location.port || '80'), - useSSL: isSSL - }; -} - -const nakamaConfig = getNakamaConfig(); -const NAKAMA_HOST = nakamaConfig.host; -const NAKAMA_PORT = nakamaConfig.port; -const NAKAMA_USE_SSL = nakamaConfig.useSSL; +const NAKAMA_HOST = import.meta.env.VITE_NAKAMA_HOST || 'localhost'; +const NAKAMA_PORT = import.meta.env.VITE_NAKAMA_PORT || '80'; +const NAKAMA_USE_SSL = import.meta.env.VITE_NAKAMA_USE_SSL === 'true'; // Polling interval for games lists (ms) export const GAMES_POLL_INTERVAL = 30000; diff --git a/frontend/src/lib/stores/watchSync.js b/frontend/src/lib/stores/watchSync.js index 24883d7..e4ba1c0 100644 --- a/frontend/src/lib/stores/watchSync.js +++ b/frontend/src/lib/stores/watchSync.js @@ -1,16 +1,7 @@ import { writable, derived, get } from 'svelte/store'; import { browser } from '$app/environment'; -// Dynamically detect WebSocket URL from browser location -function getWsUrl() { - if (!browser) { - return import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; - } - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - return `${protocol}//${window.location.host}/ws`; -} - -const WS_URL = getWsUrl(); +const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; const SYNC_INTERVAL = 5000; // Sync every 5 seconds (server pushes every 1s anyway) const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync) const LEAD_IN_DURATION = 3000; // 3 seconds lead-in for buffering diff --git a/frontend/src/lib/websocket.js b/frontend/src/lib/websocket.js index 283bc2e..0c2f941 100644 --- a/frontend/src/lib/websocket.js +++ b/frontend/src/lib/websocket.js @@ -1,5 +1,3 @@ -import { browser } from '$app/environment'; - let ws = null; let reconnectTimeout = null; let reconnectAttempts = 0; @@ -7,17 +5,7 @@ const MAX_RECONNECT_ATTEMPTS = 10; const BASE_RECONNECT_DELAY = 1000; // 1 second const MAX_RECONNECT_DELAY = 30000; // 30 seconds -// Dynamically detect WebSocket URL from browser location -// This ensures production uses wss:// and the correct host -function getWebSocketURL() { - if (!browser) { - return import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; - } - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - return `${protocol}//${window.location.host}/ws`; -} - -const WS_URL = getWebSocketURL(); +const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; /** * Calculate exponential backoff delay with jitter diff --git a/frontend/src/routes/[realm]/live/+page.svelte b/frontend/src/routes/[realm]/live/+page.svelte index 729fdaf..c47dc94 100644 --- a/frontend/src/routes/[realm]/live/+page.svelte +++ b/frontend/src/routes/[realm]/live/+page.svelte @@ -29,24 +29,7 @@ } const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088'; - const WEBRTC_PORT = import.meta.env.VITE_WEBRTC_PORT || '3333'; - - // Helper functions for dynamic host/protocol detection - function getStreamHost() { - if (!browser) return 'localhost'; - return window.location.hostname; - } - - function getStreamProtocol(secure = false) { - if (!browser) return secure ? 'https' : 'http'; - return window.location.protocol === 'https:' ? 'https' : 'http'; - } - - function getWsProtocol() { - if (!browser) return 'ws'; - return window.location.protocol === 'https:' ? 'wss' : 'ws'; - } - + let player; let realm = null; let streamKey = ''; @@ -312,28 +295,23 @@ } const sources = []; - + if (streamKey) { - // Dynamic URLs based on current page host/protocol - const host = getStreamHost(); - const httpProto = getStreamProtocol(); - const wsProto = getWsProtocol(); - // Add all sources - LLHLS first (default), then HLS, then WebRTC as fallback sources.push( { type: 'hls', - file: `${httpProto}://${host}:${STREAM_PORT}/app/${streamKey}/llhls.m3u8?token=${viewerToken}`, + file: `http://localhost:${STREAM_PORT}/app/${streamKey}/llhls.m3u8?token=${viewerToken}`, label: 'LLHLS (Low Latency)' }, { type: 'hls', - file: `${httpProto}://${host}:${STREAM_PORT}/app/${streamKey}/ts:playlist.m3u8?token=${viewerToken}`, + file: `http://localhost:${STREAM_PORT}/app/${streamKey}/ts:playlist.m3u8?token=${viewerToken}`, label: 'HLS (Standard)' }, { type: 'webrtc', - file: `${wsProto}://${host}:${WEBRTC_PORT}/app/${streamKey}`, + file: `ws://localhost:3333/app/${streamKey}`, label: 'WebRTC (Ultra Low Latency)' } ); diff --git a/frontend/src/routes/forums/[slug]/+page.svelte b/frontend/src/routes/forums/[slug]/+page.svelte index d578667..c8da091 100644 --- a/frontend/src/routes/forums/[slug]/+page.svelte +++ b/frontend/src/routes/forums/[slug]/+page.svelte @@ -244,7 +244,7 @@ const formData = new FormData(); formData.append('banner', file); - const response = await fetch(`/api/forums/${forum.id}/banner`, { + const response = await fetch(`/api/forums/${$page.params.slug}/banner`, { method: 'POST', credentials: 'include', body: formData @@ -274,7 +274,7 @@ bannerError = ''; try { - const response = await fetch(`/api/forums/${forum.id}/banner`, { + const response = await fetch(`/api/forums/${$page.params.slug}/banner`, { method: 'DELETE', credentials: 'include' }); @@ -351,7 +351,7 @@ bannerError = ''; try { - const response = await fetch(`/api/forums/${forum.id}/banner/position`, { + const response = await fetch(`/api/forums/${$page.params.slug}/banner/position`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', @@ -396,7 +396,7 @@ bannerError = ''; try { - const response = await fetch(`/api/forums/${forum.id}/title-color`, { + const response = await fetch(`/api/forums/${$page.params.slug}/title-color`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', diff --git a/frontend/src/routes/my-realms/+page.svelte b/frontend/src/routes/my-realms/+page.svelte index 286d928..f01768a 100644 --- a/frontend/src/routes/my-realms/+page.svelte +++ b/frontend/src/routes/my-realms/+page.svelte @@ -1,14 +1,10 @@