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)}
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 }); }