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':
|
case 'news':
|
||||||
require __DIR__ . '/../api/endpoints/news.php';
|
require __DIR__ . '/../api/endpoints/news.php';
|
||||||
break;
|
break;
|
||||||
|
case 'sites':
|
||||||
|
require __DIR__ . '/../api/endpoints/sites.php';
|
||||||
|
break;
|
||||||
case "agent":
|
case "agent":
|
||||||
require __DIR__ . '/../api/endpoints/agent.php';
|
require __DIR__ . '/../api/endpoints/agent.php';
|
||||||
break;
|
break;
|
||||||
|
|||||||
+138
-8
@@ -771,16 +771,17 @@ body::after{
|
|||||||
<!-- Tab Panel -->
|
<!-- Tab Panel -->
|
||||||
<div class="panel" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
<div class="panel" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
||||||
<div class="tab-bar">
|
<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('alerts')">ALERTS</div>
|
||||||
<div class="tab" onclick="switchTab('news')">NEWS</div>
|
<div class="tab" onclick="switchTab('news')">NEWS</div>
|
||||||
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
|
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
|
||||||
|
<div class="tab" onclick="switchTab('sites')">SITES</div>
|
||||||
</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 id="vm-list"><div class="loading-shimmer"></div></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 id="ha-list"><div class="loading-shimmer"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-alerts" class="tab-pane" style="overflow-y:auto;flex:1">
|
<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="tab-agents" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||||
<div id="agents-list"><div class="loading-shimmer"></div></div>
|
<div id="agents-list"><div class="loading-shimmer"></div></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -924,7 +928,6 @@ function showApp(name, greeting) {
|
|||||||
refreshAll();
|
refreshAll();
|
||||||
refreshTimer = setInterval(refreshAll, 10000); // every 10s
|
refreshTimer = setInterval(refreshAll, 10000); // every 10s
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
loadProxmox();
|
|
||||||
loadHA();
|
loadHA();
|
||||||
checkAgentStatus();
|
checkAgentStatus();
|
||||||
loadAgents();
|
loadAgents();
|
||||||
@@ -1102,7 +1105,6 @@ async function refreshAll() {
|
|||||||
|
|
||||||
// Refresh right-panel tabs every 3rd tick (~30s)
|
// Refresh right-panel tabs every 3rd tick (~30s)
|
||||||
if (_refreshTick % 3 === 0) {
|
if (_refreshTick % 3 === 0) {
|
||||||
try { await loadProxmox(); } catch(e) {}
|
|
||||||
try { await loadHA(); } catch(e) {}
|
try { await loadHA(); } catch(e) {}
|
||||||
try { await loadAlerts(); } catch(e) {}
|
try { await loadAlerts(); } catch(e) {}
|
||||||
try { await loadAgents(); } 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">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">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>
|
<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>
|
${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])=>`<div class="val-row"><div class="lbl">${k.replace('.com','')}</div><div class="val">${v}</div></div>`).join('')}` : ''}
|
${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 === 'news') loadNews();
|
||||||
if (name === 'agents') loadAgents();
|
if (name === 'agents') loadAgents();
|
||||||
if (name === 'alerts') loadAlerts();
|
if (name === 'alerts') loadAlerts();
|
||||||
|
if (name === 'sites') loadSites();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CHAT ──────────────────────────────────────────────────────────────
|
// ── CHAT ──────────────────────────────────────────────────────────────
|
||||||
@@ -1895,6 +1898,133 @@ document.addEventListener('click', function(e) {
|
|||||||
document.getElementById('agentModal').classList.remove('open');
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user