diff --git a/public/index.html b/public/index.html index 07dc0c3..ef597bf 100644 --- a/public/index.html +++ b/public/index.html @@ -519,6 +519,7 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
S3 Storage
UDP Relay
+
AMPP
Users
Folders
@@ -557,6 +558,26 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
+ +
+
AMPP Configuration
+
+ + +
Your Grass Valley AMPP regional endpoint
+
+
+ + +
+
+
+ + +
+
+
+
User Management
@@ -692,7 +713,7 @@ function showApp() { document.getElementById('header-user').textContent = currentUser; if (currentRole === 'admin') { document.querySelectorAll('.admin-only').forEach(e => e.classList.remove('hidden')); - loadS3Config(); loadRelayConfig(); loadUsers(); + loadS3Config(); loadRelayConfig(); loadAmppConfig(); loadUsers(); } loadFolders(); loadAmppJobs(); @@ -732,11 +753,12 @@ function switchPage(name) { function switchAdminTab(name) { document.querySelectorAll('.admin-tab').forEach((t,i) => { - t.classList.toggle('active', ['s3','relay','users','folders'][i] === name); + t.classList.toggle('active', ['s3','relay','ampp','users','folders'][i] === name); }); document.querySelectorAll('.admin-panel').forEach(p => p.classList.toggle('active', p.id === `admin-${name}`)); if (name === 'users') loadUsers(); if (name === 'folders') loadAdminFolders(); + if (name === 'ampp') loadAmppConfig(); } // ============================================================ @@ -1037,6 +1059,41 @@ async function saveRelay() { } function setRelayDot(c,t){document.getElementById('relay-dot').className=`relay-dot ${c}`;document.getElementById('relay-status-text').textContent=t;} +// ============================================================ +// AMPP CONFIG ADMIN +// ============================================================ +async function loadAmppConfig() { + try { + const d=await api('GET','/api/ampp/config'); if(!d.success)return; + document.getElementById('ampp-base-url').value=d.config.baseUrl||''; + const hint=document.getElementById('ampp-key-hint'); + hint.textContent=d.config.apiKeyExists?'API key is saved (leave blank to keep)':'No API key saved yet'; + } catch(_){} +} +async function testAmpp() { + const s=document.getElementById('ampp-status'); + s.className='status-msg loading'; s.textContent='🔍 Testing AMPP connection…'; + try { + const body={baseUrl:document.getElementById('ampp-base-url').value.trim()}; + const k=document.getElementById('ampp-api-key').value.trim(); + if(k) body.apiKey=k; + const d=await api('POST','/api/ampp/test',body); + s.className=`status-msg ${d.success?'success':'error'}`; s.textContent=d.success?`✅ ${d.message}`:`❌ ${d.error}`; + } catch(e){s.className='status-msg error';s.textContent=`❌ ${e.message}`;} +} +async function saveAmpp() { + const s=document.getElementById('ampp-status'); + s.className='status-msg loading'; s.textContent='💾 Saving…'; + try { + const body={baseUrl:document.getElementById('ampp-base-url').value.trim()}; + const k=document.getElementById('ampp-api-key').value.trim(); + if(k) body.apiKey=k; + const d=await api('PUT','/api/ampp/config',body); + s.className=`status-msg ${d.success?'success':'error'}`; s.textContent=d.success?'✅ AMPP configuration saved':`❌ ${d.error}`; + if(d.success){document.getElementById('ampp-api-key').value='';loadAmppConfig();} + } catch(e){s.className='status-msg error';s.textContent=`❌ ${e.message}`;} +} + // ============================================================ // USERS ADMIN // ============================================================ diff --git a/server.js b/server.js index 043634d..406dbc9 100644 --- a/server.js +++ b/server.js @@ -533,16 +533,18 @@ app.get("/api/udp/relay/health", async (req, res) => { }); // ==================== AMPP JOB MONITORING ==================== -const AMPP_BASE = process.env.AMPP_BASE_URL || "https://us-east-1.gvampp.com"; -const AMPP_API_KEY = process.env.AMPP_API_KEY || ""; +const AMPP_BASE_DEFAULT = process.env.AMPP_BASE_URL || "https://us-east-1.gvampp.com"; +function getAmppBase() { return (db.amppConfig?.baseUrl || AMPP_BASE_DEFAULT).trim(); } +function getAmppApiKey() { return db.amppConfig?.apiKey || process.env.AMPP_API_KEY || ""; } + let amppToken = null; let amppTokenExpiry = 0; async function getAmppToken() { if (amppToken && Date.now() < amppTokenExpiry - 60000) return amppToken; - const r = await fetch(`${AMPP_BASE}/identity/connect/token`, { + const r = await fetch(`${getAmppBase()}/identity/connect/token`, { method: "POST", - headers: { "Authorization": `Basic ${AMPP_API_KEY}`, "Content-Type": "application/x-www-form-urlencoded" }, + headers: { "Authorization": `Basic ${getAmppApiKey()}`, "Content-Type": "application/x-www-form-urlencoded" }, body: "grant_type=client_credentials&scope=platform", }); if (!r.ok) throw new Error(`AMPP auth failed (${r.status})`); @@ -552,13 +554,63 @@ async function getAmppToken() { return amppToken; } +// ---- AMPP Config admin endpoints ---- +app.get("/api/ampp/config", requireAdmin, (req, res) => { + const cfg = db.amppConfig || {}; + res.json({ + success: true, + config: { + baseUrl: cfg.baseUrl || AMPP_BASE_DEFAULT, + apiKeyExists: !!(cfg.apiKey), + } + }); +}); + +app.put("/api/ampp/config", requireAdmin, (req, res) => { + const { baseUrl, apiKey } = req.body; + if (!db.amppConfig) db.amppConfig = {}; + if (baseUrl !== undefined) db.amppConfig.baseUrl = baseUrl.trim(); + if (apiKey) db.amppConfig.apiKey = apiKey.trim(); + amppToken = null; amppTokenExpiry = 0; // invalidate cached token + saveData(db); + res.json({ success: true, message: "AMPP configuration saved" }); +}); + +app.post("/api/ampp/test", requireAdmin, async (req, res) => { + const { baseUrl, apiKey } = req.body; + const testBase = (baseUrl || getAmppBase()).trim(); + const testApiKey = (apiKey || getAmppApiKey()).trim(); + if (!testApiKey) return res.status(400).json({ success: false, error: "API key is required to test" }); + try { + const r = await fetch(`${testBase}/identity/connect/token`, { + method: "POST", + headers: { "Authorization": `Basic ${testApiKey}`, "Content-Type": "application/x-www-form-urlencoded" }, + body: "grant_type=client_credentials&scope=platform", + signal: AbortSignal.timeout(8000), + }); + if (!r.ok) { + const text = await r.text().catch(() => ""); + return res.status(400).json({ success: false, error: `Authentication failed (HTTP ${r.status})${text ? ': ' + text.slice(0, 120) : ''}` }); + } + const data = await r.json(); + if (!data.access_token) return res.status(400).json({ success: false, error: "No access token returned — check credentials" }); + res.json({ success: true, message: "AMPP connection confirmed! Authentication successful." }); + } catch (err) { + let msg = err.message; + if (err.name === "TimeoutError" || err.name === "AbortError") msg = "Request timed out — check the base URL"; + else if (msg.includes("ECONNREFUSED")) msg = "Connection refused — check the base URL and network"; + else if (msg.includes("ENOTFOUND")) msg = "Host not found — check the base URL"; + res.status(400).json({ success: false, error: msg }); + } +}); + app.get("/api/ampp/jobs", requireAuth, async (req, res) => { - if (!AMPP_API_KEY) return res.status(503).json({ success: false, error: "AMPP API key not configured" }); + if (!getAmppApiKey()) return res.status(503).json({ success: false, error: "AMPP API key not configured" }); try { const token = await getAmppToken(); const limit = parseInt(req.query.limit) || 100; const skip = parseInt(req.query.skip) || 0; - const url = `${AMPP_BASE}/api/v1/queue/job/jobs/querypage?skip=${skip}&limit=${limit}&sort=created:dateTime&asc=false`; + const url = `${getAmppBase()}/api/v1/queue/job/jobs/querypage?skip=${skip}&limit=${limit}&sort=created:dateTime&asc=false`; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), 15000); const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, body: "{}", signal: controller.signal }); @@ -579,10 +631,10 @@ app.get("/api/ampp/jobs", requireAuth, async (req, res) => { }); app.get("/api/ampp/jobs/:jobId", requireAuth, async (req, res) => { - if (!AMPP_API_KEY) return res.status(503).json({ success: false, error: "AMPP API key not configured" }); + if (!getAmppApiKey()) return res.status(503).json({ success: false, error: "AMPP API key not configured" }); try { const token = await getAmppToken(); - const r = await fetch(`${AMPP_BASE}/api/v1/queue/job/jobs/${encodeURIComponent(req.params.jobId)}`, { headers: { "Authorization": `Bearer ${token}` } }); + const r = await fetch(`${getAmppBase()}/api/v1/queue/job/jobs/${encodeURIComponent(req.params.jobId)}`, { headers: { "Authorization": `Bearer ${token}` } }); if (!r.ok) return res.status(r.status).json({ success: false, error: `AMPP ${r.status}` }); res.json({ success: true, job: await r.json() }); } catch (err) { res.status(500).json({ success: false, error: err.message }); }