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:
@@ -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']);
|
||||
@@ -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