From 2c5459af82931309df0c3f75ff8791371c5ff037 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Fri, 29 May 2026 19:28:24 +0000 Subject: [PATCH] =?UTF-8?q?Add=20Sites=20Manager=20to=20JARVIS=20=E2=80=94?= =?UTF-8?q?=20centralized=20email=20settings=20for=20all=20sites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/endpoints/sites.php | 158 ++++++++++++++++++++++++++++++++++++++++ public_html/api.php | 3 + public_html/index.html | 146 +++++++++++++++++++++++++++++++++++-- 3 files changed, 299 insertions(+), 8 deletions(-) create mode 100644 api/endpoints/sites.php diff --git a/api/endpoints/sites.php b/api/endpoints/sites.php new file mode 100644 index 0000000..b724e0f --- /dev/null +++ b/api/endpoints/sites.php @@ -0,0 +1,158 @@ + [ + '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']); diff --git a/public_html/api.php b/public_html/api.php index d095de5..a5d580b 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -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; diff --git a/public_html/index.html b/public_html/index.html index 2ebd803..dce28c1 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -771,16 +771,17 @@ body::after{
-
PROXMOX
-
HOME
+ +
HOME
ALERTS
NEWS
AGENTS
+
SITES
-
+
-
+
@@ -792,6 +793,9 @@ body::after{
+
+
+
@@ -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) {
DISK
${d.disk_used_pct??'--'}
UPTIME
${d.uptime??'--'}
LOAD
${d.load_1m??'--'}
- ${d.sites && Object.keys(d.sites).length ? `
SITES:
- ${Object.entries(d.sites).map(([k,v])=>`
${k.replace('.com','')}
${v}
`).join('')}` : ''} + ${d.sites && Object.keys(d.sites).length ? `
WEBSITES:
+ ${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`
${lbl}
${v.toUpperCase()}
`}).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 = '
'; + const res = await api('sites'); + if (!res.success) { el.innerHTML = '
FAILED TO LOAD SITE SETTINGS
'; 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 = ` +
SITES MANAGER
+ + +
+
▸ CYBERMAIL API KEY — ALL SITES
+
+ + +
+
+
`; + + // Per-site cards + for (const [id, s] of Object.entries(sites)) { + html += ` +
+
+
+
${s.name.toUpperCase()}
+
${s.url}
+
+
+
+
+
FROM EMAIL
+ +
+
+
FROM NAME
+ +
+
+
ADMIN NOTIFICATION EMAIL
+ +
+
+
+ + +
+
`; + } + + 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'); + } +} +