Sites Manager: replace small panel with full-screen modal overlay

This commit is contained in:
2026-05-29 19:31:34 +00:00
parent 2c5459af82
commit 0f82fb9e85
+83 -79
View File
@@ -793,9 +793,7 @@ 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>
@@ -828,6 +826,35 @@ body::after{
</div> </div>
</div> </div>
</div> </div>
<div id="sitesModal" style="position:fixed;inset:0;background:rgba(0,0,0,0.92);z-index:9999;display:none;align-items:flex-start;justify-content:center;padding:24px;overflow-y:auto">
<div style="background:var(--panel-bg);border:1px solid var(--panel-border);width:100%;max-width:960px;font-family:var(--font-mono)">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--panel-border)">
<div style="color:var(--cyan);font-size:0.75rem;letter-spacing:4px">◈ SITES MANAGER — EMAIL SETTINGS</div>
<button onclick="closeSitesModal()" style="background:transparent;border:1px solid var(--panel-border);color:var(--text-dim);cursor:pointer;font-family:var(--font-mono);font-size:0.6rem;padding:4px 12px;letter-spacing:2px">✕ CLOSE</button>
</div>
<div style="padding:20px 24px">
<!-- Global API Key -->
<div style="background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.2);padding:16px;margin-bottom:20px">
<div style="color:var(--cyan);font-size:0.62rem;letter-spacing:3px;margin-bottom:10px">▸ CYBERMAIL API KEY — PUSH TO ALL SITES</div>
<div style="display:flex;gap:10px;align-items:center">
<input id="global-api-key" type="password"
style="flex:1;background:#0a0f1a;border:1px solid rgba(0,212,255,0.25);color:var(--text);font-family:var(--font-mono);font-size:0.7rem;padding:8px 12px;outline:none"
placeholder="sk_live_...">
<button onclick="pushApiKey()"
style="background:rgba(0,212,255,0.12);border:1px solid var(--cyan);color:var(--cyan);font-family:var(--font-mono);font-size:0.6rem;letter-spacing:2px;padding:8px 18px;cursor:pointer;white-space:nowrap">
PUSH TO ALL
</button>
</div>
<div id="push-status" style="font-size:0.6rem;color:var(--text-dim);margin-top:6px;min-height:16px"></div>
</div>
<!-- Site Cards Grid -->
<div id="sites-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
<div style="grid-column:1/-1;color:var(--text-dim);font-size:0.65rem">LOADING...</div>
</div>
</div>
</div>
</div>
<div id="agentModal"> <div id="agentModal">
<div class="agent-modal-box"> <div class="agent-modal-box">
<button class="agent-modal-close" onclick="document.getElementById('agentModal').classList.remove('open')">&#x2715; CLOSE</button> <button class="agent-modal-close" onclick="document.getElementById('agentModal').classList.remove('open')">&#x2715; CLOSE</button>
@@ -1487,7 +1514,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(); if (name === 'sites') { openSitesModal(); return; }
} }
// ── CHAT ────────────────────────────────────────────────────────────── // ── CHAT ──────────────────────────────────────────────────────────────
@@ -1899,129 +1926,106 @@ document.addEventListener('click', function(e) {
}); });
// ── SITES MANAGER ──────────────────────────────────────────────────── // ── SITES MANAGER ────────────────────────────────────────────────────
let sitesData = {}; let sitesData = {};
function openSitesModal() {
document.getElementById('sitesModal').style.display = 'flex';
loadSites();
}
function closeSitesModal() {
document.getElementById('sitesModal').style.display = 'none';
}
// Close on backdrop click
document.getElementById('sitesModal').addEventListener('click', function(e) {
if (e.target === this) closeSitesModal();
});
async function loadSites() { async function loadSites() {
const el = document.getElementById('sites-content'); document.getElementById('sites-grid').innerHTML = '<div style="grid-column:1/-1;color:var(--text-dim);font-size:0.65rem;letter-spacing:2px">LOADING SITE SETTINGS...</div>';
el.innerHTML = '<div class="loading-shimmer"></div>';
const res = await api('sites'); 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; } if (!res.success) {
document.getElementById('sites-grid').innerHTML = '<div style="grid-column:1/-1;color:#f44;font-size:0.65rem">FAILED TO LOAD SETTINGS</div>';
return;
}
sitesData = res.sites; sitesData = res.sites;
renderSites(); // Pre-fill global key from first site
const firstKey = Object.values(res.sites)[0]?.api_key || '';
document.getElementById('global-api-key').value = firstKey;
renderSiteCards();
} }
function renderSites() { function renderSiteCards() {
const el = document.getElementById('sites-content'); const grid = document.getElementById('sites-grid');
const sites = sitesData; let html = '';
for (const [id, s] of Object.entries(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 += ` 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="background:rgba(0,212,255,0.02);border:1px solid rgba(0,212,255,0.12);padding:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> <div style="margin-bottom:12px">
<div> <div style="color:var(--cyan);font-size:0.65rem;letter-spacing:2px;margin-bottom:2px">${s.name.toUpperCase()}</div>
<div style="font-family:var(--font-mono);font-size:0.6rem;letter-spacing:1px;color:var(--cyan)">${s.name.toUpperCase()}</div> <div style="color:var(--text-dim);font-size:0.58rem">${s.url}</div>
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)">${s.url}</div>
</div> </div>
</div> <div style="margin-bottom:10px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px"> <div style="color:var(--text-dim);font-size:0.58rem;letter-spacing:1px;margin-bottom:4px">FROM EMAIL</div>
<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 || ''}" <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"> 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.65rem;padding:6px 10px;outline:none;box-sizing:border-box">
</div> </div>
<div> <div style="margin-bottom:10px">
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);margin-bottom:3px">FROM NAME</div> <div style="color:var(--text-dim);font-size:0.58rem;letter-spacing:1px;margin-bottom:4px">FROM NAME</div>
<input id="${id}-from_name" type="text" value="${s.from_name || ''}" <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"> 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.65rem;padding:6px 10px;outline:none;box-sizing:border-box">
</div> </div>
<div style="grid-column:1/-1"> <div style="margin-bottom:12px">
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);margin-bottom:3px">ADMIN NOTIFICATION EMAIL</div> <div style="color:var(--text-dim);font-size:0.58rem;letter-spacing:1px;margin-bottom:4px">ADMIN NOTIFICATION EMAIL</div>
<input id="${id}-admin_email" type="text" value="${s.admin_email || ''}" <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"> 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.65rem;padding:6px 10px;outline:none;box-sizing:border-box">
</div> </div>
</div> <div style="display:flex;align-items:center;gap:10px">
<div style="display:flex;align-items:center;gap:8px">
<button onclick="saveSite('${id}')" <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"> 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.58rem;letter-spacing:2px;padding:6px 16px;cursor:pointer">
SAVE SAVE
</button> </button>
<span id="${id}-status" style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)"></span> <span id="${id}-status" style="font-size:0.58rem;color:var(--text-dim)"></span>
</div> </div>
</div>`; </div>`;
} }
grid.innerHTML = html;
el.innerHTML = html;
} }
async function pushApiKey() { async function pushApiKey() {
const key = document.getElementById('global-api-key').value.trim(); const key = document.getElementById('global-api-key').value.trim();
const status = document.getElementById('push-status'); const status = document.getElementById('push-status');
if (!key) { status.textContent = '✗ API key required'; status.style.color = '#f44'; return; } if (!key) { status.style.color='#f44'; status.textContent='✗ API KEY REQUIRED'; return; }
status.textContent = 'PUSHING...'; status.style.color='var(--text-dim)'; status.textContent='PUSHING TO ALL SITES...';
status.style.color = 'var(--text-dim)';
const res = await api('sites', 'POST', {action:'push_key', api_key:key}); const res = await api('sites', 'POST', {action:'push_key', api_key:key});
if (res.success) { if (res.success) {
const ok = Object.values(res.results).filter(Boolean).length; const ok = Object.values(res.results).filter(Boolean).length;
const total = Object.keys(res.results).length; const total = Object.keys(res.results).length;
status.style.color = ok === total ? 'var(--cyan)' : '#fa0'; status.style.color = ok === total ? 'var(--cyan)' : '#fa0';
status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`; status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`;
// Update local cache
for (const id of Object.keys(sitesData)) sitesData[id].api_key = key; for (const id of Object.keys(sitesData)) sitesData[id].api_key = key;
} else { } else {
status.style.color = '#f44'; status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED');
status.textContent = '✗ ' + (res.error || 'FAILED');
} }
} }
async function saveSite(id) { async function saveSite(id) {
const status = document.getElementById(id + '-status'); const status = document.getElementById(id + '-status');
status.textContent = 'SAVING...'; status.style.color='var(--text-dim)'; status.textContent='SAVING...';
status.style.color = 'var(--text-dim)'; const res = await api('sites', 'POST', {
const payload = {
action: 'save', action: 'save',
site: id, site: id,
from_email: document.getElementById(id+'-from_email').value.trim(), from_email: document.getElementById(id+'-from_email').value.trim(),
from_name: document.getElementById(id+'-from_name').value.trim(), from_name: document.getElementById(id+'-from_name').value.trim(),
admin_email: document.getElementById(id+'-admin_email').value.trim(), admin_email: document.getElementById(id+'-admin_email').value.trim(),
}; });
const res = await api('sites', 'POST', payload);
if (res.success) { if (res.success) {
status.style.color = 'var(--cyan)'; status.style.color='var(--cyan)'; status.textContent='✓ SAVED';
status.textContent = '✓ SAVED';
setTimeout(() => { status.textContent=''; }, 3000); 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 { } else {
status.style.color = '#f44'; status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED');
status.textContent = '✗ ' + (res.error || 'FAILED');
} }
} }