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
+158
View File
@@ -0,0 +1,158 @@
<?php
/**
* JARVIS Sites Manager — read/write email settings across all hosted sites
*/
// ── Site definitions ────────────────────────────────────────────────
$SITES = [
'tomsjavajive' => [
'name' => "Tom's Java Jive",
'url' => 'https://tomsjavajive.com',
'type' => 'db',
'db' => ['host'=>'localhost','name'=>'toms_tjj_db','user'=>'toms_tjj_user','pass'=>'+60wlPc+55e@gFq4'],
'keys' => ['api_key'=>'cybermail_api_key','from_email'=>'cybermail_from_email','from_name'=>'cybermail_from_name','admin_email'=>'smtp_admin_email'],
],
'tomtomgames' => [
'name' => 'TomTomGames',
'url' => 'https://tomtomgames.com',
'type' => 'file',
'file' => '/home/tomtomgames.com/includes/config.php',
'keys' => ['api_key'=>'CYBERMAIL_API_KEY','from_email'=>'SMTP_FROM','from_name'=>'SMTP_FROM_NAME','admin_email'=>'ADMIN_EMAIL'],
],
'epictravelexpeditions' => [
'name' => 'Epic Travel Expeditions',
'url' => 'https://epictravelexpeditions.com',
'type' => 'file',
'file' => '/home/epictravelexpeditions.com/public_html/api/config.php',
'keys' => ['api_key'=>'CYBERMAIL_API_KEY','from_email'=>'MAIL_FROM','from_name'=>'MAIL_FROM_NAME','admin_email'=>'ADMIN_EMAIL'],
],
'parkerslingshot' => [
'name' => 'Parker Slingshot',
'url' => 'https://parkerslingshot.epictravelexpeditions.com',
'type' => 'file',
'file' => '/home/epictravelexpeditions.com/parkerslingshot/db.php',
'keys' => ['api_key'=>'CYBERMAIL_API_KEY','from_email'=>'MAIL_FROM','from_name'=>'MAIL_FROM_NAME','admin_email'=>'ADMIN_EMAIL'],
],
'parkerslingshotrentals' => [
'name' => 'Parker Slingshot Rentals',
'url' => 'https://parkerslingshotrentals.com',
'type' => 'file',
'file' => '/home/parkerslingshotrentals.com/public_html/db.php',
'keys' => ['api_key'=>'CYBERMAIL_API_KEY','from_email'=>'MAIL_FROM','from_name'=>'MAIL_FROM_NAME','admin_email'=>'ADMIN_EMAIL'],
],
];
// ── Helpers ──────────────────────────────────────────────────────────
function fileGet(string $file, string $constant): string {
if (!file_exists($file)) return '';
$content = file_get_contents($file);
if (preg_match("/define\s*\(\s*['\"]" . preg_quote($constant, '/') . "['\"],\s*['\"]([^'\"]*)['\"].*?\)/s", $content, $m))
return $m[1];
return '';
}
function fileSet(string $file, string $constant, string $value): bool {
if (!file_exists($file)) return false;
$content = file_get_contents($file);
$safe = str_replace("'", "\\'", $value);
$new = preg_replace(
"/define\s*\(\s*['\"]" . preg_quote($constant, '/') . "['\"],\s*['\"][^'\"]*['\"](\s*)\)/",
"define('" . $constant . "', '" . $safe . "'$1)",
$content
);
if ($new === null || $new === $content) return false;
return file_put_contents($file, $new) !== false;
}
function dbGet(array $db, string $key): string {
try {
$pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4",
$db['user'], $db['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$s = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key=?");
$s->execute([$key]);
$row = $s->fetch(PDO::FETCH_ASSOC);
if (!$row) return '';
$decoded = json_decode($row['setting_value'], true);
return $decoded ?? $row['setting_value'];
} catch (Exception $e) { return ''; }
}
function dbSet(array $db, string $key, string $value): bool {
try {
$pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4",
$db['user'], $db['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$existing = $pdo->prepare("SELECT id FROM settings WHERE setting_key=?");
$existing->execute([$key]);
if ($existing->fetch()) {
$pdo->prepare("UPDATE settings SET setting_value=? WHERE setting_key=?")->execute([json_encode($value), $key]);
} else {
$pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?,?)")->execute([$key, json_encode($value)]);
}
return true;
} catch (Exception $e) { return false; }
}
function siteGet(array $site, string $field): string {
$constant = $site['keys'][$field] ?? '';
if (!$constant) return '';
if ($site['type'] === 'db') return dbGet($site['db'], $constant);
return fileGet($site['file'], $constant);
}
function siteSet(array $site, string $field, string $value): bool {
$constant = $site['keys'][$field] ?? '';
if (!$constant) return false;
if ($site['type'] === 'db') return dbSet($site['db'], $constant, $value);
return fileSet($site['file'], $constant, $value);
}
// ── Router ───────────────────────────────────────────────────────────
if ($method === 'GET') {
$result = [];
foreach ($SITES as $id => $site) {
$result[$id] = [
'name' => $site['name'],
'url' => $site['url'],
'api_key' => siteGet($site, 'api_key'),
'from_email' => siteGet($site, 'from_email'),
'from_name' => siteGet($site, 'from_name'),
'admin_email'=> siteGet($site, 'admin_email'),
];
}
echo json_encode(['success' => true, 'sites' => $result]);
exit;
}
if ($method === 'POST') {
$action = $data['action'] ?? '';
$siteId = $data['site'] ?? '';
$results = [];
if ($action === 'push_key') {
// Push API key to all sites at once
$apiKey = trim($data['api_key'] ?? '');
if (!$apiKey) { echo json_encode(['success'=>false,'error'=>'API key required']); exit; }
foreach ($SITES as $id => $site) {
$results[$id] = siteSet($site, 'api_key', $apiKey);
}
echo json_encode(['success'=>true,'results'=>$results]);
exit;
}
if ($action === 'save' && $siteId && isset($SITES[$siteId])) {
$site = $SITES[$siteId];
$fields = ['api_key', 'from_email', 'from_name', 'admin_email'];
foreach ($fields as $field) {
if (isset($data[$field])) {
$results[$field] = siteSet($site, $field, trim($data[$field]));
}
}
echo json_encode(['success'=>true,'results'=>$results]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
exit;
}
echo json_encode(['success'=>false,'error'=>'Method not allowed']);
+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>