Add Sites Manager to JARVIS — centralized email settings for all sites

This commit is contained in:
2026-05-29 19:28:24 +00:00
parent 3bcd3dcb65
commit 2c5459af82
3 changed files with 299 additions and 8 deletions
+3
View File
@@ -81,6 +81,9 @@ switch ($endpoint) {
case 'news':
require __DIR__ . '/../api/endpoints/news.php';
break;
case 'sites':
require __DIR__ . '/../api/endpoints/sites.php';
break;
case "agent":
require __DIR__ . '/../api/endpoints/agent.php';
break;
+138 -8
View File
@@ -771,16 +771,17 @@ body::after{
<!-- Tab Panel -->
<div class="panel" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
<div class="tab-bar">
<div class="tab active" onclick="switchTab('vms')">PROXMOX</div>
<div class="tab" onclick="switchTab('ha')">HOME</div>
<div class="tab active" onclick="switchTab('ha')">HOME</div>
<div class="tab" onclick="switchTab('alerts')">ALERTS</div>
<div class="tab" onclick="switchTab('news')">NEWS</div>
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
<div class="tab" onclick="switchTab('sites')">SITES</div>
</div>
<div id="tab-vms" class="tab-pane active" style="overflow-y:auto;flex:1">
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
<div id="vm-list"><div class="loading-shimmer"></div></div>
</div>
<div id="tab-ha" class="tab-pane" style="overflow-y:auto;flex:1">
<div id="tab-ha" class="tab-pane active" style="overflow-y:auto;flex:1">
<div id="ha-list"><div class="loading-shimmer"></div></div>
</div>
<div id="tab-alerts" class="tab-pane" style="overflow-y:auto;flex:1">
@@ -792,6 +793,9 @@ body::after{
<div id="tab-agents" class="tab-pane" style="overflow-y:auto;flex:1">
<div id="agents-list"><div class="loading-shimmer"></div></div>
</div>
<div id="tab-sites" class="tab-pane" style="overflow-y:auto;flex:1;padding:8px">
<div id="sites-content"><div class="loading-shimmer"></div></div>
</div>
</div>
</div>
</div>
@@ -924,7 +928,6 @@ function showApp(name, greeting) {
refreshAll();
refreshTimer = setInterval(refreshAll, 10000); // every 10s
loadNetwork();
loadProxmox();
loadHA();
checkAgentStatus();
loadAgents();
@@ -1102,7 +1105,6 @@ async function refreshAll() {
// Refresh right-panel tabs every 3rd tick (~30s)
if (_refreshTick % 3 === 0) {
try { await loadProxmox(); } catch(e) {}
try { await loadHA(); } catch(e) {}
try { await loadAlerts(); } catch(e) {}
try { await loadAgents(); } catch(e) {}
@@ -1197,8 +1199,8 @@ function renderDO(d) {
<div class="val-row"><div class="lbl">DISK</div><div class="val">${d.disk_used_pct??'--'}</div></div>
<div class="val-row"><div class="lbl">UPTIME</div><div class="val">${d.uptime??'--'}</div></div>
<div class="val-row"><div class="lbl">LOAD</div><div class="val">${d.load_1m??'--'}</div></div>
${d.sites && Object.keys(d.sites).length ? `<div style="margin-top:8px;font-family:var(--font-mono);font-size:0.65rem;color:var(--text-dim)">SITES:</div>
${Object.entries(d.sites).map(([k,v])=>`<div class="val-row"><div class="lbl">${k.replace('.com','')}</div><div class="val">${v}</div></div>`).join('')}` : ''}
${d.sites && Object.keys(d.sites).length ? `<div style="margin-top:8px;font-family:var(--font-mono);font-size:0.65rem;color:var(--text-dim)">WEBSITES:</div>
${Object.entries(d.sites).map(([k,v])=>{const cls=v==='up'?'ok':v==='down'?'danger':'warn';const lbl=k.replace(/^https?:\/\//,'').replace(/\.com$/,'').replace(/\.orbishosting$/,'');return`<div class="val-row"><div class="lbl" style="font-size:.62rem">${lbl}</div><div class="val ${cls}">${v.toUpperCase()}</div></div>`}).join('')}` : ''}
`;
}
@@ -1485,6 +1487,7 @@ function switchTab(name) {
if (name === 'news') loadNews();
if (name === 'agents') loadAgents();
if (name === 'alerts') loadAlerts();
if (name === 'sites') loadSites();
}
// ── CHAT ──────────────────────────────────────────────────────────────
@@ -1895,6 +1898,133 @@ document.addEventListener('click', function(e) {
document.getElementById('agentModal').classList.remove('open');
});
// ── SITES MANAGER ────────────────────────────────────────────────────
let sitesData = {};
async function loadSites() {
const el = document.getElementById('sites-content');
el.innerHTML = '<div class="loading-shimmer"></div>';
const res = await api('sites');
if (!res.success) { el.innerHTML = '<div style="color:var(--text-dim);font-family:var(--font-mono);font-size:0.7rem;padding:8px">FAILED TO LOAD SITE SETTINGS</div>'; return; }
sitesData = res.sites;
renderSites();
}
function renderSites() {
const el = document.getElementById('sites-content');
const sites = sitesData;
// Get the shared API key from first site
const firstSite = Object.values(sites)[0] || {};
const apiKey = firstSite.api_key || '';
let html = `
<div style="font-family:var(--font-mono);font-size:0.6rem;letter-spacing:2px;color:var(--cyan);padding:4px 0 8px">SITES MANAGER</div>
<!-- Global: Push API Key -->
<div style="background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.15);border-radius:4px;padding:10px;margin-bottom:10px">
<div style="font-family:var(--font-mono);font-size:0.58rem;letter-spacing:2px;color:var(--cyan);margin-bottom:6px">▸ CYBERMAIL API KEY — ALL SITES</div>
<div style="display:flex;gap:6px;align-items:center">
<input id="global-api-key" type="password" value="${apiKey}"
style="flex:1;background:#0a0f1a;border:1px solid rgba(0,212,255,0.2);color:var(--text);font-family:var(--font-mono);font-size:0.65rem;padding:5px 8px;border-radius:3px;outline:none"
placeholder="sk_live_...">
<button onclick="pushApiKey()"
style="background:rgba(0,212,255,0.1);border:1px solid var(--cyan);color:var(--cyan);font-family:var(--font-mono);font-size:0.55rem;letter-spacing:2px;padding:5px 10px;cursor:pointer;border-radius:3px;white-space:nowrap">
PUSH TO ALL
</button>
</div>
<div id="push-status" style="font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);margin-top:4px;min-height:14px"></div>
</div>`;
// Per-site cards
for (const [id, s] of Object.entries(sites)) {
html += `
<div style="background:rgba(0,212,255,0.02);border:1px solid rgba(0,212,255,0.1);border-radius:4px;padding:10px;margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div>
<div style="font-family:var(--font-mono);font-size:0.6rem;letter-spacing:1px;color:var(--cyan)">${s.name.toUpperCase()}</div>
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)">${s.url}</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
<div>
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);margin-bottom:3px">FROM EMAIL</div>
<input id="${id}-from_email" type="text" value="${s.from_email || ''}"
style="width:100%;background:#0a0f1a;border:1px solid rgba(0,212,255,0.15);color:var(--text);font-family:var(--font-mono);font-size:0.6rem;padding:4px 7px;border-radius:3px;outline:none;box-sizing:border-box">
</div>
<div>
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);margin-bottom:3px">FROM NAME</div>
<input id="${id}-from_name" type="text" value="${s.from_name || ''}"
style="width:100%;background:#0a0f1a;border:1px solid rgba(0,212,255,0.15);color:var(--text);font-family:var(--font-mono);font-size:0.6rem;padding:4px 7px;border-radius:3px;outline:none;box-sizing:border-box">
</div>
<div style="grid-column:1/-1">
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);margin-bottom:3px">ADMIN NOTIFICATION EMAIL</div>
<input id="${id}-admin_email" type="text" value="${s.admin_email || ''}"
style="width:100%;background:#0a0f1a;border:1px solid rgba(0,212,255,0.15);color:var(--text);font-family:var(--font-mono);font-size:0.6rem;padding:4px 7px;border-radius:3px;outline:none;box-sizing:border-box">
</div>
</div>
<div style="display:flex;align-items:center;gap:8px">
<button onclick="saveSite('${id}')"
style="background:rgba(0,212,255,0.08);border:1px solid rgba(0,212,255,0.3);color:var(--cyan);font-family:var(--font-mono);font-size:0.52rem;letter-spacing:2px;padding:4px 12px;cursor:pointer;border-radius:3px">
SAVE
</button>
<span id="${id}-status" style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)"></span>
</div>
</div>`;
}
el.innerHTML = html;
}
async function pushApiKey() {
const key = document.getElementById('global-api-key').value.trim();
const status = document.getElementById('push-status');
if (!key) { status.textContent = '✗ API key required'; status.style.color = '#f44'; return; }
status.textContent = 'PUSHING...';
status.style.color = 'var(--text-dim)';
const res = await api('sites', 'POST', {action:'push_key', api_key:key});
if (res.success) {
const ok = Object.values(res.results).filter(Boolean).length;
const total = Object.keys(res.results).length;
status.style.color = ok === total ? 'var(--cyan)' : '#fa0';
status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`;
// Update local cache
for (const id of Object.keys(sitesData)) sitesData[id].api_key = key;
} else {
status.style.color = '#f44';
status.textContent = '✗ ' + (res.error || 'FAILED');
}
}
async function saveSite(id) {
const status = document.getElementById(id + '-status');
status.textContent = 'SAVING...';
status.style.color = 'var(--text-dim)';
const payload = {
action: 'save',
site: id,
from_email: document.getElementById(id + '-from_email').value.trim(),
from_name: document.getElementById(id + '-from_name').value.trim(),
admin_email: document.getElementById(id + '-admin_email').value.trim(),
};
const res = await api('sites', 'POST', payload);
if (res.success) {
status.style.color = 'var(--cyan)';
status.textContent = '✓ SAVED';
setTimeout(() => { status.textContent = ''; }, 3000);
// Update local cache
if (sitesData[id]) {
sitesData[id].from_email = payload.from_email;
sitesData[id].from_name = payload.from_name;
sitesData[id].admin_email = payload.admin_email;
}
} else {
status.style.color = '#f44';
status.textContent = '✗ ' + (res.error || 'FAILED');
}
}
</script>
</body>
</html>