fix: split relay URL into internal + public for browser clients

The relay container is reached internally via docker service name
(http://dragon-wind-relay:3001) but browsers need the public address.
Previously the internal URL was sent to clients causing UDP uploads to fail.

- Add publicRelayUrl field to relay config (server + admin UI)
- /api/udp/session now returns publicRelayUrl to browser clients
- Internal relayUrl still used for server-side health checks
- Falls back to internal URL if public not set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-04-06 22:02:14 -04:00
parent b3f669afe4
commit 42fead82aa
2 changed files with 19 additions and 6 deletions

View file

@ -552,7 +552,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
<div class="admin-panel" id="admin-relay">
<div class="section-title">UDP Relay Configuration</div>
<div class="relay-status-indicator"><div class="relay-dot grey" id="relay-dot"></div><span id="relay-status-text">Not checked</span></div>
<div class="form-group"><label class="form-label">Relay Server URL</label><input class="form-input" id="relay-url" type="url" placeholder="https://relay.yourdomain.com"/><div class="form-hint">Base URL of your Dragon Wind UDP Relay container</div></div>
<div class="form-group"><label class="form-label">Internal Relay URL</label><input class="form-input" id="relay-url" type="url" placeholder="http://dragon-wind-relay:3001"/><div class="form-hint">Internal URL the server uses to reach the relay container (Docker service name or localhost)</div></div>
<div class="form-group"><label class="form-label">Public Relay URL <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--text-dim)">(what browsers connect to)</span></label><input class="form-input" id="relay-public-url" type="url" placeholder="http://vpm.broadcastmgmt.cloud:3001"/><div class="form-hint">The externally reachable URL for the relay — sent to uploaders' browsers. Must be reachable on port 3001 from the internet.</div></div>
<div class="form-group"><label class="form-label">UDP Data Port</label><input class="form-input" id="relay-udp-port" type="number" placeholder="5000" min="1024" max="65535"/></div>
<div class="btn-row">
<button class="btn-secondary" onclick="testRelay()">🔍 Test Relay</button>
@ -1208,7 +1209,12 @@ async function saveS3() {
// RELAY ADMIN
// ============================================================
async function loadRelayConfig() {
try { const d=await api('GET','/api/relay/config'); if(!d.success)return; document.getElementById('relay-url').value=d.config.relayUrl||''; document.getElementById('relay-udp-port').value=d.config.udpPort||5000; } catch(_){}
try {
const d=await api('GET','/api/relay/config'); if(!d.success)return;
document.getElementById('relay-url').value=d.config.relayUrl||'';
document.getElementById('relay-public-url').value=d.config.publicRelayUrl||'';
document.getElementById('relay-udp-port').value=d.config.udpPort||5000;
} catch(_){}
}
async function testRelay() {
const s=document.getElementById('relay-status');
@ -1223,7 +1229,7 @@ async function saveRelay() {
const s=document.getElementById('relay-status');
s.className='status-msg loading'; s.textContent='💾 Saving…';
try {
const d=await api('PUT','/api/relay/config',{relayUrl:document.getElementById('relay-url').value.trim(),udpPort:parseInt(document.getElementById('relay-udp-port').value)});
const d=await api('PUT','/api/relay/config',{relayUrl:document.getElementById('relay-url').value.trim(),publicRelayUrl:document.getElementById('relay-public-url').value.trim(),udpPort:parseInt(document.getElementById('relay-udp-port').value)});
s.className=`status-msg ${d.success?'success':'error'}`; s.textContent=d.success?'✅ Relay configuration saved':`❌ ${d.error}`;
} catch(e){s.className='status-msg error';s.textContent=`❌ ${e.message}`;}
}

View file

@ -483,13 +483,18 @@ app.post("/api/s3/test", requireAdmin, async (req, res) => {
// ==================== RELAY CONFIG (Admin) ====================
app.get("/api/relay/config", requireAdmin, (req, res) => {
const cfg = db.relayConfig || {};
res.json({ success: true, config: { relayUrl: cfg.relayUrl || "", udpPort: cfg.udpPort || 5000 } });
res.json({ success: true, config: {
relayUrl: cfg.relayUrl || "",
publicRelayUrl: cfg.publicRelayUrl || "",
udpPort: cfg.udpPort || 5000
}});
});
app.put("/api/relay/config", requireAdmin, (req, res) => {
const { relayUrl, udpPort } = req.body;
const { relayUrl, publicRelayUrl, udpPort } = req.body;
if (!db.relayConfig) db.relayConfig = {};
db.relayConfig.relayUrl = (relayUrl || "").trim();
db.relayConfig.publicRelayUrl = (publicRelayUrl || "").trim();
db.relayConfig.udpPort = parseInt(udpPort) || 5000;
saveData(db);
res.json({ success: true, message: "Relay configuration saved" });
@ -588,6 +593,8 @@ app.post("/api/udp/session", requireAuth, (req, res) => {
const relayUrl = db.relayConfig?.relayUrl || "";
const udpPort = db.relayConfig?.udpPort || 5000;
if (!relayUrl) return res.status(503).json({ success: false, error: "UDP relay not configured. Go to Admin → Relay Settings." });
// Public URL is what the browser connects to — falls back to internal if not set
const publicRelayUrl = (db.relayConfig?.publicRelayUrl || "").trim() || relayUrl;
const sessionId = crypto.randomBytes(16).toString("hex");
const key = (prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename);
udpSessions.set(sessionId, {
@ -598,7 +605,7 @@ app.post("/api/udp/session", requireAuth, (req, res) => {
});
// Cleanup after 2 hours
setTimeout(() => udpSessions.delete(sessionId), 2 * 60 * 60 * 1000);
res.json({ success: true, sessionId, relayUrl, udpPort, key, s3Bucket: db.s3Config?.bucket || "" });
res.json({ success: true, sessionId, relayUrl: publicRelayUrl, udpPort, key, s3Bucket: db.s3Config?.bucket || "" });
});
app.get("/api/udp/session/:id", requireAuth, (req, res) => {