mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Add Sites Manager to JARVIS — centralized email settings for all sites
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user