Compare commits

..

33 Commits

Author SHA1 Message Date
oliver 69618bfbd5 Update index.html 2026-06-16 15:08:49 -03:00
oliver a4f0f330ad Update index.html 2026-06-16 14:39:39 -03:00
oliver 62298d1f28 Update index.html 2026-06-16 07:45:26 -03:00
oliver 421215d42d active 2026-06-16 05:44:55 -03:00
oliver 16bec05592 pivot 2026-06-15 19:06:39 -03:00
oliver a5e642bf6d Update app.js 2026-06-15 14:17:12 -03:00
oliver 46d8158c67 new server 2026-06-15 13:35:34 -03:00
oliver 7be5f2a160 Update app.js 2026-06-15 09:59:59 -03:00
oliver 4272ee4f84 Update app.js 2026-06-15 09:59:11 -03:00
oliver de69f1184a Update app.js 2026-06-15 09:40:18 -03:00
oliver 9bdfc0be70 Update app.js 2026-06-15 09:19:27 -03:00
oliver 145079e344 Update app.js 2026-06-15 09:12:22 -03:00
oliver 9448cef053 Update app.js 2026-06-14 20:59:14 -03:00
oliver 4ec38cc43a Update app.js 2026-06-14 20:58:59 -03:00
oliver 99811f9f49 refferal 2026-06-14 20:56:04 -03:00
oliver 897a8b459c Update app.js 2026-06-14 20:34:37 -03:00
oliver 412e93ad83 hold 2026-06-14 20:30:11 -03:00
oliver 677cf54d60 sort 2026-06-14 20:24:16 -03:00
oliver f4c1a5cf96 design 2026-06-14 20:20:36 -03:00
oliver 0502073bd9 Update app.js 2026-06-14 20:13:23 -03:00
oliver cc1bff3be0 crm 2026-06-14 20:10:20 -03:00
oliver 880577e498 Create favicon.ico 2026-06-14 19:58:16 -03:00
oliver 6b8e04f3e0 jj 2026-06-09 19:27:15 -03:00
oliver 230129e05b . 2026-06-09 19:21:14 -03:00
oliver 50479d0755 dash 2026-06-09 19:09:13 -03:00
oliver 0084845480 fix 2026-06-08 17:36:46 -03:00
oliver eacad5ee4e new api 2026-06-08 17:30:59 -03:00
oliver 57c3992d86 copy 2026-06-08 17:00:32 -03:00
oliver 4c9cbe9d2c API Doc 2026-06-08 16:59:32 -03:00
oliver 76401469ad Update styles.css 2026-06-08 09:27:02 -03:00
oliver 14e1c45faf filter app 2026-06-06 06:39:06 -03:00
oliver 951a2f2a06 typo 2026-06-06 06:15:20 -03:00
oliver c70cbeae10 typo 2026-06-06 06:13:27 -03:00
5 changed files with 3822 additions and 387 deletions
+2494
View File
File diff suppressed because it is too large Load Diff
+583 -109
View File
@@ -1,16 +1,16 @@
/* ─── Constants ──────────────────────────────────────────────────── */ /* ─── Constants ──────────────────────────────────────────────────── */
const WEBHOOK_URL = const API_BASE = "https://n8n.derez.ai/webhook";
"https://n8n.derez.ai/webhook/4ad0c89f-ab8c-45ee-bad1-9321ce94dd64";
const INSTANCES_URL = /* ─── API route paths ────────────────────────────────────────────── */
"https://n8n.derez.ai/webhook/bb369f27-244c-4f8a-869b-f787050619a2"; const ROUTES = {
const BACKUP_URL = auth: "/account/login",
"https://n8n.derez.ai/webhook/69c0c632-df3b-457f-affe-b725f217f9a2"; dns: "/admin/dns",
const SERVERS_URL = instances: "/admin/instance",
"https://n8n.derez.ai/webhook/78c0c2b8-e497-4d44-8f26-fec34217513c"; backup: "/admin/backup-repo",
const KEYS_URL = servers: "/admin/server",
"https://n8n.derez.ai/webhook/a001374f-d8c0-4430-9b47-9f1e9ab134c3"; keys: "/admin/key",
const AUTH_URL = crm: "/crm",
"https://n8n.derez.ai/webhook/e256310a-6627-45ba-a221-599751943fe6"; };
/* ─── State ──────────────────────────────────────────────────────── */ /* ─── State ──────────────────────────────────────────────────────── */
let dnsRecords = []; let dnsRecords = [];
@@ -20,6 +20,9 @@ let backupRecords = [];
let serversData = []; let serversData = [];
let currentServerIdx = 0; let currentServerIdx = 0;
let keysData = []; let keysData = [];
let crmRecords = [];
let crmSortKey = null;
let crmSortDir = 1;
let currentSession = null; let currentSession = null;
let currentEmail = null; let currentEmail = null;
@@ -55,8 +58,11 @@ async function apiFetch(url, options = {}) {
} }
/* ─── API helpers ────────────────────────────────────────────────── */ /* ─── API helpers ────────────────────────────────────────────────── */
function getApiUrl() { function getApiBase() {
return localStorage.getItem("al_url") || WEBHOOK_URL; return (localStorage.getItem("al_url") || API_BASE).replace(/\/$/, "");
}
function apiUrl(route) {
return getApiBase() + route;
} }
function apiHeaders() { function apiHeaders() {
return { "Content-Type": "application/json" }; return { "Content-Type": "application/json" };
@@ -108,7 +114,7 @@ async function doSignIn() {
btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Signing in…`; btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Signing in…`;
try { try {
const res = await fetch(AUTH_URL, { const res = await fetch(apiUrl(ROUTES.auth), {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
@@ -139,7 +145,7 @@ async function doSignIn() {
/* ─── Sign Out ───────────────────────────────────────────────────── */ /* ─── Sign Out ───────────────────────────────────────────────────── */
function doSignOut() { function doSignOut() {
if (currentSession) { if (currentSession) {
fetch(AUTH_URL, { fetch(apiUrl(ROUTES.auth), {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -163,8 +169,6 @@ function doSignOut() {
function updateSidebarUser() { function updateSidebarUser() {
document.getElementById("sidebar-username").textContent = document.getElementById("sidebar-username").textContent =
currentEmail || "Not signed in"; currentEmail || "Not signed in";
const settingsEl = document.getElementById("settings-session-email");
if (settingsEl) settingsEl.textContent = currentEmail || "—";
} }
/* ─── Navigation ─────────────────────────────────────────────────── */ /* ─── Navigation ─────────────────────────────────────────────────── */
@@ -185,10 +189,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
instances: "Instances", instances: "Instances",
backup: "Backup", backup: "Backup",
servers: "Servers", servers: "Servers",
settings: "Settings",
"coming-soon-firewall": "Firewall",
"coming-soon-ssl": "SSL / TLS",
keys: "Keys", keys: "Keys",
sales: "Sales",
crm: "CRM",
}; };
document.getElementById("page-title").textContent = labels[key] || key; document.getElementById("page-title").textContent = labels[key] || key;
@@ -210,6 +213,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
} else if (key === "keys") { } else if (key === "keys") {
topbar.style.display = ""; topbar.style.display = "";
btnRefresh.onclick = keysLoadRecords; btnRefresh.onclick = keysLoadRecords;
} else if (key === "crm") {
topbar.style.display = "";
btnRefresh.onclick = crmLoadRecords;
} else { } else {
topbar.style.display = "none"; topbar.style.display = "none";
} }
@@ -227,6 +233,9 @@ document.querySelectorAll(".nav-item[data-section]").forEach((el) => {
if (key === "keys" && keysData.length === 0) { if (key === "keys" && keysData.length === 0) {
keysLoadRecords(); keysLoadRecords();
} }
if (key === "crm" && crmRecords.length === 0) {
crmLoadRecords();
}
}); });
}); });
@@ -269,7 +278,7 @@ async function dnsLoadRecords() {
const tbody = document.getElementById("dns-table-body"); const tbody = document.getElementById("dns-table-body");
tbody.innerHTML = `<tr><td colspan="6"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`; tbody.innerHTML = `<tr><td colspan="6"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`;
try { try {
const res = await apiFetch(getApiUrl(), { const res = await apiFetch(apiUrl(ROUTES.dns), {
headers: apiHeaders(), headers: apiHeaders(),
}); });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
@@ -302,7 +311,7 @@ function dnsRender() {
.getElementById("dns-filter-type") .getElementById("dns-filter-type")
.value.toUpperCase(); .value.toUpperCase();
const HIDDEN_NAMES = ["fr", "n8n", "admin", "@"]; const HIDDEN_NAMES = ["fr", "n8n", "usa", "admin", "app", "@"];
const filtered = dnsRecords.filter((r) => { const filtered = dnsRecords.filter((r) => {
const name = r.name.toLowerCase(); const name = r.name.toLowerCase();
@@ -383,7 +392,7 @@ async function dnsDeleteRecord(btn) {
if (!ok) return; if (!ok) return;
try { try {
const res = await apiFetch(getApiUrl(), { const res = await apiFetch(apiUrl(ROUTES.dns), {
method: "DELETE", method: "DELETE",
headers: apiHeaders(), headers: apiHeaders(),
body: JSON.stringify({ body: JSON.stringify({
@@ -421,7 +430,7 @@ async function dnsAddRecord() {
btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Adding…`; btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Adding…`;
try { try {
const res = await apiFetch(getApiUrl(), { const res = await apiFetch(apiUrl(ROUTES.dns), {
method: "POST", method: "POST",
headers: apiHeaders(), headers: apiHeaders(),
body: JSON.stringify({ body: JSON.stringify({
@@ -452,13 +461,17 @@ function settingsSave() {
const url = document.getElementById("setting-url").value.trim(); const url = document.getElementById("setting-url").value.trim();
if (url) { if (url) {
localStorage.setItem("al_url", url); localStorage.setItem("al_url", url);
toast("Webhook URL saved", "success"); toast("API base URL saved", "success");
} else toast("Enter a URL to save", "warning"); } else {
localStorage.removeItem("al_url");
document.getElementById("setting-url").value = "";
toast("Reverted to default: " + API_BASE, "info");
}
} }
async function settingsTest() { async function settingsTest() {
try { try {
const res = await apiFetch(getApiUrl(), { const res = await apiFetch(apiUrl(ROUTES.dns), {
headers: apiHeaders(), headers: apiHeaders(),
}); });
if (res.ok) toast("Connection OK — HTTP " + res.status, "success"); if (res.ok) toast("Connection OK — HTTP " + res.status, "success");
@@ -519,9 +532,11 @@ function stripQuotes(s) {
/* ─── Instances — Load ───────────────────────────────────────── */ /* ─── Instances — Load ───────────────────────────────────────── */
async function instancesLoadRecords() { async function instancesLoadRecords() {
const tbody = document.getElementById("inst-table-body"); const tbody = document.getElementById("inst-table-body");
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`; tbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`;
try { try {
const res = await apiFetch(INSTANCES_URL, { headers: apiHeaders() }); const res = await apiFetch(apiUrl(ROUTES.instances), {
headers: apiHeaders(),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const json = await res.json(); const json = await res.json();
// Normalise: [{data:[...]}, ...], plain array, {data:[...]}, or single object // Normalise: [{data:[...]}, ...], plain array, {data:[...]}, or single object
@@ -551,12 +566,10 @@ function instancesRender() {
const filtered = instancesRecords.filter((r) => { const filtered = instancesRecords.filter((r) => {
if (!search) return true; if (!search) return true;
return ( return (
stripQuotes(r.uuid).toLowerCase().includes(search) || r.uuid?.toLowerCase().includes(search) ||
stripQuotes(r.agent).toLowerCase().includes(search) || r.agent?.toLowerCase().includes(search) ||
stripQuotes(r.domain).toLowerCase().includes(search) || r.email?.toLowerCase().includes(search) ||
stripQuotes(r.server ?? "") (r.server ?? "").toLowerCase().includes(search)
.toLowerCase()
.includes(search)
); );
}); });
@@ -567,7 +580,7 @@ function instancesRender() {
const tbody = document.getElementById("inst-table-body"); const tbody = document.getElementById("inst-table-body");
if (filtered.length === 0) { if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state"> tbody.innerHTML = `<tr><td colspan="8"><div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/> <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/> <polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>
@@ -579,43 +592,28 @@ function instancesRender() {
tbody.innerHTML = filtered tbody.innerHTML = filtered
.map((r) => { .map((r) => {
const uuid = escHtml(r.uuid); const uuid = escHtml(r.UUID ?? "");
const agent = escHtml(stripQuotes(r.agent)); const agent = escHtml(r.agent);
const domain = escHtml(stripQuotes(r.domain)); const product = escHtml(r.product ?? "");
const sshPort = escHtml(stripQuotes(r.ssh_port)); const server = escHtml(r.server ?? "");
const ram = escHtml(stripQuotes(r.ram)); const email = escHtml(r.email ?? "");
const cpu = escHtml(stripQuotes(r.cpu)); const period = escHtml(r.period ?? "");
const server = escHtml(stripQuotes(r.server ?? "")); const sshStatus = r.SSH ? "✓" : "";
const created = escHtml(stripQuotes(r.created ?? "")); const backupStatus = r.BACKUP ? "B" : "";
const createdStatus = r.created ? "C" : "";
const statusParts = [sshStatus, backupStatus, createdStatus].filter(
Boolean,
);
const status = statusParts.length ? statusParts.join(" ") : "—";
return `<tr> return `<tr>
<td><span class="mono">${uuid}</span></td> <td><span class="mono">${uuid}</span></td>
<td>${agent}</td> <td>${agent}</td>
<td><span class="mono">${domain}</span></td> <td>${product}</td>
<td class="mono">${sshPort}</td>
<td>${ram}</td>
<td>${cpu}</td>
<td>${server}</td> <td>${server}</td>
<td>${created}</td> <td>${email}</td>
<td>${period}</td>
<td><span class="mono">${status}</span></td>
<td style="white-space:nowrap; display:flex; gap:6px;"> <td style="white-space:nowrap; display:flex; gap:6px;">
<button class="btn btn-ghost btn-sm"
data-domain="${domain}"
data-port="${sshPort}"
onclick="instancesCopySSH(this)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
Copy SSH
</button>
<button class="btn btn-ghost btn-sm"
data-uuid="${uuid}"
onclick="instancesOpenEdit(this)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edit
</button>
<button class="btn btn-danger btn-sm" <button class="btn btn-danger btn-sm"
data-uuid="${uuid}" data-uuid="${uuid}"
data-agent="${agent}" data-agent="${agent}"
@@ -663,7 +661,7 @@ async function instancesDelete(btn) {
if (!ok) return; if (!ok) return;
try { try {
const res = await apiFetch(INSTANCES_URL, { const res = await apiFetch(apiUrl(ROUTES.instances), {
method: "DELETE", method: "DELETE",
headers: apiHeaders(), headers: apiHeaders(),
body: JSON.stringify({ uuid }), body: JSON.stringify({ uuid }),
@@ -721,7 +719,7 @@ async function instancesSaveEdit() {
btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Saving…`; btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Saving…`;
try { try {
const res = await apiFetch(INSTANCES_URL, { const res = await apiFetch(apiUrl(ROUTES.instances), {
method: "PUT", method: "PUT",
headers: apiHeaders(), headers: apiHeaders(),
body: JSON.stringify({ body: JSON.stringify({
@@ -753,7 +751,9 @@ async function backupLoadRecords() {
const tbody = document.getElementById("backup-table-body"); const tbody = document.getElementById("backup-table-body");
tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`; tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`;
try { try {
const res = await apiFetch(BACKUP_URL, { headers: apiHeaders() }); const res = await apiFetch(apiUrl(ROUTES.backup), {
headers: apiHeaders(),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const json = await res.json(); const json = await res.json();
// Normalise: // Normalise:
@@ -848,7 +848,7 @@ async function backupDelete(btn) {
if (!ok) return; if (!ok) return;
try { try {
const res = await apiFetch(BACKUP_URL, { const res = await apiFetch(apiUrl(ROUTES.backup), {
method: "DELETE", method: "DELETE",
headers: apiHeaders(), headers: apiHeaders(),
body: JSON.stringify({ id }), body: JSON.stringify({ id }),
@@ -886,19 +886,49 @@ async function serversLoadData() {
document.getElementById("server-dashboard").innerHTML = document.getElementById("server-dashboard").innerHTML =
`<div class="empty-state"><div class="spinner"></div><p style="margin-top:12px">Loading…</p></div>`; `<div class="empty-state"><div class="spinner"></div><p style="margin-top:12px">Loading…</p></div>`;
try { try {
const res = await apiFetch(SERVERS_URL, { headers: apiHeaders() }); /* ─── Step 1: Fetch server list with seats ──────────────── */
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); const seatRes = await fetch("https://n8n.derez.ai/webhook/server");
const json = await res.json(); if (!seatRes.ok) throw new Error(`Seat API HTTP ${seatRes.status}`);
const raw = Array.isArray(json) ? json : [json]; const seatJson = await seatRes.json();
serversData = raw const seatList = Array.isArray(seatJson?.data)
.map((item) => { ? seatJson.data
try { : Array.isArray(seatJson)
return typeof item.text === "string" ? JSON.parse(item.text) : item; ? seatJson
} catch { : [];
return null; if (!seatList.length) throw new Error("No servers in seat endpoint");
/* ─── Step 2: Fetch full dashboard for each server ─────── */
serversData = [];
for (const seat of seatList) {
const serverName = seat.server;
try {
const res = await apiFetch(
apiUrl(ROUTES.servers) + "?server=" + encodeURIComponent(serverName),
{
headers: apiHeaders(),
},
);
if (res.ok) {
const json = await res.json();
const raw = Array.isArray(json) ? json : [json];
for (const item of raw) {
let parsed;
try {
parsed =
typeof item.text === "string" ? JSON.parse(item.text) : item;
} catch {
continue;
}
if (!parsed) continue;
parsed.seats = seat;
serversData.push(parsed);
}
} }
}) } catch (_) {
.filter(Boolean); /* skip servers that fail */
}
}
if (serversData.length === 0) throw new Error("No server data in response"); if (serversData.length === 0) throw new Error("No server data in response");
currentServerIdx = 0; currentServerIdx = 0;
serversRenderTabs(); serversRenderTabs();
@@ -932,7 +962,9 @@ function serversRenderTabs() {
.map((s, i) => { .map((s, i) => {
const status = s.summary?.overall_status ?? "unknown"; const status = s.summary?.overall_status ?? "unknown";
const color = dotColor[status] ?? "var(--text-muted)"; const color = dotColor[status] ?? "var(--text-muted)";
const name = escHtml(s.server?.name ?? `Server ${i + 1}`); const name = escHtml(
s.server?.name ?? s.seats?.server ?? `Server ${i + 1}`,
);
return `<div class="server-tab${i === currentServerIdx ? " active" : ""}" onclick="serversSelectTab(${i})"> return `<div class="server-tab${i === currentServerIdx ? " active" : ""}" onclick="serversSelectTab(${i})">
<span class="server-tab-dot" style="background:${color}"></span> <span class="server-tab-dot" style="background:${color}"></span>
${name} ${name}
@@ -1059,7 +1091,7 @@ function serversRenderDashboard(idx) {
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px"> <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px">
<div> <div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
<span style="font-size:20px;font-weight:700">${escHtml(server.name)}</span> <span style="font-size:20px;font-weight:700">${escHtml(server.name ?? s.seats?.server)}</span>
<span class="badge badge-${escHtml(summary.overall_status)}">${summary.overall_status.toUpperCase()}</span> <span class="badge badge-${escHtml(summary.overall_status)}">${summary.overall_status.toUpperCase()}</span>
</div> </div>
<div class="text-muted" style="font-size:12px"> <div class="text-muted" style="font-size:12px">
@@ -1071,9 +1103,6 @@ function serversRenderDashboard(idx) {
<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.07em;margin-top:3px">Score</div> <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.07em;margin-top:3px">Score</div>
</div> </div>
</div> </div>
<p style="font-size:13px;color:var(--text-muted);line-height:1.65;border-top:1px solid var(--border);padding-top:12px;margin:0">
${escHtml(summary.short_summary)}
</p>
</div> </div>
<!-- ─ CPU / Memory / Disk ───────────────────────────────── --> <!-- ─ CPU / Memory / Disk ───────────────────────────────── -->
@@ -1137,7 +1166,7 @@ function serversRenderDashboard(idx) {
<!-- ─ Security + Containers ──────────────────────────────── --> <!-- ─ Security + Containers ──────────────────────────────── -->
<div class="dash-grid"> <div class="dash-grid">
<div class="card dash-span-2"> <div class="card">
<div class="card-title" style="margin-bottom:14px"> <div class="card-title" style="margin-bottom:14px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
@@ -1155,26 +1184,46 @@ function serversRenderDashboard(idx) {
<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.07em;margin-bottom:6px">Exposed Ports</div> <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.07em;margin-bottom:6px">Exposed Ports</div>
<div>${portsHtml}</div> <div>${portsHtml}</div>
<div style="font-size:12px;color:var(--text-muted);margin-top:8px">Vuln score: ${security.vulnerability_score ?? "—"}</div> <div style="font-size:12px;color:var(--text-muted);margin-top:8px">Vuln score: ${security.vulnerability_score ?? "—"}</div>
</div>
</div> </div>
</div> </div>
${
security.risk_flags?.length
? `
<div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--border);display:flex;gap:6px;flex-wrap:wrap">
${security.risk_flags.map((f) => `<span class="badge badge-medium">${escHtml(f)}</span>`).join("")}
</div>`
: ""
}
</div>
<div class="card"> <div class="card">
<div class="card-title" style="margin-bottom:14px"> <div class="card-title" style="margin-bottom:14px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/> <rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/> </svg>
</svg> Seats
Containers </div>
${
s.seats
? `
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:14px">
<div>
<div class="dash-stat">${s.seats.Seats ?? "—"}</div>
<div class="dash-stat-sub">Total</div>
</div>
<div>
<div class="dash-stat">${s.seats["Seats taken"] ?? "—"}</div>
<div class="dash-stat-sub">Taken</div>
</div>
<div>
<div class="dash-stat" style="color:${(s.seats["Seats Available"] ?? 0) < 2 ? "var(--danger)" : (s.seats["Seats Available"] ?? 0) < 10 ? "var(--warning)" : "var(--success)"}">${s.seats["Seats Available"] ?? "—"}</div>
<div class="dash-stat-sub">Available</div>
</div>
</div>`
: `<div class="text-muted" style="font-size:13px;padding:8px 0">No seat data available</div>`
}
</div> </div>
<div class="card">
<div class="card-title" style="margin-bottom:14px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
Containers
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div> <div>
<div class="dash-stat" style="color:${containers.running > 0 ? "var(--success)" : "var(--text-muted)"}">${containers.running}</div> <div class="dash-stat" style="color:${containers.running > 0 ? "var(--success)" : "var(--text-muted)"}">${containers.running}</div>
@@ -1255,7 +1304,7 @@ async function keysLoadRecords() {
const tbody = document.getElementById("keys-table-body"); const tbody = document.getElementById("keys-table-body");
tbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`; tbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`;
try { try {
const res = await apiFetch(KEYS_URL, { headers: apiHeaders() }); const res = await apiFetch(apiUrl(ROUTES.keys), { headers: apiHeaders() });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
// Support both [{data:[...]}, ...] and {data:[...]} and flat array // Support both [{data:[...]}, ...] and {data:[...]} and flat array
@@ -1335,7 +1384,7 @@ async function keysDelete(btn) {
if (!ok) return; if (!ok) return;
try { try {
const res = await apiFetch(KEYS_URL, { const res = await apiFetch(apiUrl(ROUTES.keys), {
method: "DELETE", method: "DELETE",
headers: apiHeaders(), headers: apiHeaders(),
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
@@ -1348,6 +1397,431 @@ async function keysDelete(btn) {
} }
} }
/* ─── CRM — Load ────────────────────────────────────────────────── */
async function crmLoadRecords() {
const tbody = document.getElementById("crm-table-body");
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state"><div class="spinner"></div><p style="margin-top:12px;">Loading…</p></div></td></tr>`;
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
headers: apiHeaders(),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const json = await res.json();
// Shape: {data: [{...}, ...]}
let data = json?.data ?? json?.response ?? json;
// If the outer result wraps in an extra array like [{data:[...]}] handle that too
if (Array.isArray(data) && data.length > 0 && data[0]?.data) {
data = data[0].data;
}
crmRecords = Array.isArray(data) ? data : [];
crmRender();
toast(
`Loaded ${crmRecords.length} contact${crmRecords.length !== 1 ? "s" : ""}`,
"success",
);
} catch (err) {
crmRecords = [];
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<p style="margin-top:10px; color:var(--danger);">${err.message}</p>
</div></td></tr>`;
toast("Failed to load CRM contacts: " + err.message, "error");
}
}
/* ─── CRM — Render table ────────────────────────────────────────── */
function crmRender() {
const searchName = (
document.getElementById("crm-search-name")?.value ?? ""
).toLowerCase();
const searchCompany = (
document.getElementById("crm-search-company")?.value ?? ""
).toLowerCase();
const searchEmail = (
document.getElementById("crm-search-email")?.value ?? ""
).toLowerCase();
const searchStage = (
document.getElementById("crm-search-stage")?.value ?? ""
).toLowerCase();
const searchStrategy = (
document.getElementById("crm-search-strategy")?.value ?? ""
).toLowerCase();
const searchCategory = (
document.getElementById("crm-search-category")?.value ?? ""
).toLowerCase();
const searchAffiliate = (
document.getElementById("crm-search-affiliate")?.value ?? ""
).toLowerCase();
let filtered = crmRecords.filter((r) => {
const name = (r.Name ?? "").toLowerCase();
const company = (r.Company ?? "").toLowerCase();
const email = (r.email ?? "").toLowerCase();
const stage = (r.Stage ?? "").toLowerCase();
const strategy = (r.strategy ?? "").toLowerCase();
const category = (r.category ?? "").toLowerCase();
const affiliate = (r.affiliate ?? "").toLowerCase();
return (
(!searchName || name.includes(searchName)) &&
(!searchCompany || company.includes(searchCompany)) &&
(!searchEmail || email.includes(searchEmail)) &&
(!searchStage || stage.includes(searchStage)) &&
(!searchStrategy || strategy.includes(searchStrategy)) &&
(!searchCategory || category.includes(searchCategory)) &&
(!searchAffiliate || affiliate.includes(searchAffiliate))
);
});
// Sort
if (crmSortKey) {
filtered.sort((a, b) => {
const va = (a[crmSortKey] ?? "").toString().toLowerCase();
const vb = (b[crmSortKey] ?? "").toString().toLowerCase();
if (va < vb) return -1 * crmSortDir;
if (va > vb) return 1 * crmSortDir;
return 0;
});
}
// Update sort arrows
document.querySelectorAll(".sort-arrow").forEach((el) => {
el.textContent =
el.dataset.sort === crmSortKey ? (crmSortDir === 1 ? " ▲" : " ▼") : "";
});
const total = crmRecords.length;
const countEl = document.getElementById("crm-record-count");
countEl.textContent =
filtered.length === total
? `${total} contact${total !== 1 ? "s" : ""}`
: `${filtered.length} / ${total} contacts`;
// Compute category counts from all records (unfiltered)
const catCounts = {};
for (const r of crmRecords) {
const cat = (r.category || "Uncategorized").trim();
catCounts[cat] = (catCounts[cat] || 0) + 1;
}
const catContainer = document.getElementById("crm-category-counts");
if (catContainer) {
catContainer.innerHTML = Object.entries(catCounts)
.sort((a, b) => b[1] - a[1])
.map(
([cat, cnt]) =>
`<span style="font-size:11px;color:var(--text-muted);background:var(--bg-input);border:1px solid var(--border);border-radius:4px;padding:1px 6px;white-space:nowrap">${escHtml(cat)}: ${cnt}</span>`,
)
.join("");
}
// Reset check-all state
const checkAll = document.getElementById("crm-check-all");
if (checkAll) checkAll.checked = false;
const tbody = document.getElementById("crm-table-body");
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<p>No contacts found</p>
</div></td></tr>`;
document.getElementById("crm-delete-selected-btn").disabled = true;
return;
}
tbody.innerHTML = filtered
.map((r) => {
const id = r.id;
const name = escHtml(r.Name ?? "");
const company = escHtml(r.Company ?? "");
const email = escHtml(r.email ?? "");
const stage = escHtml(r.Stage ?? "");
const strategy = escHtml(r.strategy ?? "");
const category = escHtml(r.category ?? "");
const affiliate = escHtml(r.affiliate ?? "");
const dateNext = r.date_next_contact
? formatDate(r.date_next_contact)
: "—";
// Prevent row click when clicking the checkbox
return `<tr data-crm-id="${id}" style="cursor:pointer;" onclick="crmOpenEdit(${id})">
<td style="cursor:default;" onclick="event.stopPropagation()"><input type="checkbox" class="crm-select" data-id="${id}" onchange="crmUpdateDeleteBtn()" /></td>
<td><strong>${name || "—"}</strong></td>
<td>${company || "—"}</td>
<td><a href="mailto:${email}" style="color:var(--accent)" onclick="event.stopPropagation()">${email || "—"}</a></td>
<td><span class="badge ${stage === "Hold" ? "badge-other" : "badge-green"}">${stage || "—"}</span></td>
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${strategy}">${strategy || "—"}</td>
<td>${category ? escHtml(category) : "—"}</td>
<td>${affiliate || "—"}</td>
<td class="text-muted">${dateNext}</td>
</tr>`;
})
.join("");
crmUpdateDeleteBtn();
}
/* ─── CRM — Clear all filters ──────────────────────────────────── */
function crmClearFilters() {
[
"name",
"company",
"email",
"stage",
"strategy",
"category",
"affiliate",
].forEach((f) => {
const el = document.getElementById("crm-search-" + f);
if (el) el.value = "";
});
crmRender();
}
/* ─── CRM — Sort ───────────────────────────────────────────────── */
function crmToggleSort(key) {
if (crmSortKey === key) {
crmSortDir *= -1;
} else {
crmSortKey = key;
crmSortDir = 1;
}
crmRender();
}
/* ─── CRM — Checkbox helpers ────────────────────────────────────── */
function crmUpdateDeleteBtn() {
const checked = document.querySelectorAll(".crm-select:checked").length;
const hasSel = checked > 0;
document.getElementById("crm-delete-selected-btn").disabled = !hasSel;
document.getElementById("crm-hold-btn").disabled = !hasSel;
document.getElementById("crm-active-btn").disabled = !hasSel;
const label = hasSel ? `Delete Checked (${checked})` : "Delete Checked";
document.getElementById("crm-delete-selected-btn").innerHTML =
`<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;vertical-align:middle">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg> ${label}`;
document.getElementById("crm-hold-btn").textContent = hasSel
? `Set Hold (${checked})`
: "Set Hold";
document.getElementById("crm-active-btn").textContent = hasSel
? `Set Active (${checked})`
: "Set Active";
}
function crmToggleAll() {
const checked = document.getElementById("crm-check-all").checked;
document.querySelectorAll(".crm-select").forEach((cb) => {
cb.checked = checked;
});
crmUpdateDeleteBtn();
}
/* ─── CRM — Delete single contact ──────────────────────────────── */
async function crmDeleteContact(btn) {
const id = btn.dataset.id;
const name = btn.dataset.name || `#${id}`;
const ok = await confirmDialog(
"Delete Contact",
`Delete contact “${name}” (id: ${id})? This cannot be undone.`,
);
if (!ok) return;
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
method: "DELETE",
headers: apiHeaders(),
body: JSON.stringify({ id: Number(id) }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
toast(`Contact “${name}” deleted`, "success");
await crmLoadRecords();
} catch (err) {
toast("Delete failed: " + err.message, "error");
}
}
/* ─── CRM — Delete selected contacts ────────────────────────────── */
async function crmDeleteSelected() {
const ids = Array.from(document.querySelectorAll(".crm-select:checked")).map(
(cb) => Number(cb.dataset.id),
);
if (ids.length === 0) return;
const ok = await confirmDialog(
"Delete Selected Contacts",
`Delete ${ids.length} contact${ids.length !== 1 ? "s" : ""}? This cannot be undone.`,
);
if (!ok) return;
// Delete sequentially
let failed = 0;
for (const id of ids) {
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
method: "DELETE",
headers: apiHeaders(),
body: JSON.stringify({ id }),
});
if (!res.ok) failed++;
} catch {
failed++;
}
}
if (failed === 0) {
toast(
`Deleted ${ids.length} contact${ids.length !== 1 ? "s" : ""}`,
"success",
);
} else {
toast(
`Deleted ${ids.length - failed} contact${ids.length - failed !== 1 ? "s" : ""} (${failed} failed)`,
"warning",
);
}
await crmLoadRecords();
}
/* ─── CRM — Set stage for checked contacts ──────────────────────── */
async function crmSetStageSelected(stage) {
const ids = Array.from(document.querySelectorAll(".crm-select:checked")).map(
(cb) => Number(cb.dataset.id),
);
if (ids.length === 0) return;
const ok = await confirmDialog(
`Set Stage to ${stage}`,
`Set stage to “${stage}” for ${ids.length} contact${ids.length !== 1 ? "s" : ""}?`,
stage,
);
if (!ok) return;
let failed = 0;
for (const id of ids) {
const r = crmRecords.find((c) => c.id === id);
if (!r) {
failed++;
continue;
}
const payload = {
id,
Name: r.Name ?? "",
Company: r.Company ?? "",
email: r.email ?? "",
Stage: stage,
strategy: r.strategy ?? "",
category: r.category ?? "",
affiliate: r.affiliate ?? "",
Number: r.Number ?? "",
date_next_contact: r.date_next_contact ?? null,
History: r.History ?? "",
};
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
method: "POST",
headers: apiHeaders(),
body: JSON.stringify(payload),
});
if (!res.ok) failed++;
} catch {
failed++;
}
}
if (failed === 0) {
toast(
`Stage set to “${stage}” for ${ids.length} contact${ids.length !== 1 ? "s" : ""}`,
"success",
);
} else {
toast(
`Updated ${ids.length - failed} contact${ids.length - failed !== 1 ? "s" : ""} (${failed} failed)`,
"warning",
);
}
await crmLoadRecords();
}
/* ─── CRM — Edit modal ──────────────────────────────────────────── */
function crmOpenEdit(id) {
const r = crmRecords.find((c) => c.id === id);
if (!r) return;
document.getElementById("crm-edit-id").value = r.id;
document.getElementById("crm-edit-name").value = r.Name ?? "";
document.getElementById("crm-edit-company").value = r.Company ?? "";
document.getElementById("crm-edit-email").value = r.email ?? "";
document.getElementById("crm-edit-stage").value = r.Stage ?? "";
document.getElementById("crm-edit-strategy").value = r.strategy ?? "";
document.getElementById("crm-edit-category").value = r.category ?? "";
document.getElementById("crm-edit-affiliate").value = r.affiliate ?? "";
document.getElementById("crm-edit-number").value = r.Number ?? "";
document.getElementById("crm-edit-date-next").value = r.date_next_contact
? r.date_next_contact.split("T")[0]
: "";
document.getElementById("crm-edit-history").value = r.History ?? "";
document.getElementById("crm-edit-title").textContent =
`Edit Contact — ${r.Name || r.email || `#${r.id}`}`;
document.getElementById("crm-edit-backdrop").classList.add("open");
setTimeout(() => document.getElementById("crm-edit-name").focus(), 80);
}
function crmCloseEdit() {
document.getElementById("crm-edit-backdrop").classList.remove("open");
}
document.getElementById("crm-edit-backdrop")?.addEventListener("click", (e) => {
if (e.target === e.currentTarget) crmCloseEdit();
});
async function crmSaveEdit() {
const id = Number(document.getElementById("crm-edit-id").value);
const payload = {
id,
Name: document.getElementById("crm-edit-name").value.trim(),
Company: document.getElementById("crm-edit-company").value.trim(),
email: document.getElementById("crm-edit-email").value.trim(),
Stage: document.getElementById("crm-edit-stage").value,
strategy: document.getElementById("crm-edit-strategy").value.trim(),
category: document.getElementById("crm-edit-category").value.trim(),
affiliate: document.getElementById("crm-edit-affiliate").value.trim(),
Number: document.getElementById("crm-edit-number").value.trim(),
date_next_contact:
document.getElementById("crm-edit-date-next").value || null,
History: document.getElementById("crm-edit-history").value.trim(),
};
const btn = document.getElementById("crm-save-btn");
btn.disabled = true;
const orig = btn.innerHTML;
btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;"></div> Saving…`;
try {
const res = await apiFetch(apiUrl(ROUTES.crm), {
method: "POST",
headers: apiHeaders(),
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
toast(
`Contact “${payload.Name || payload.email || `#${id}`}” updated`,
"success",
);
crmCloseEdit();
await crmLoadRecords();
} catch (err) {
toast("Save failed: " + err.message, "error");
} finally {
btn.disabled = false;
btn.innerHTML = orig;
}
}
/* ─── Init ────────────────────────────────────────────────────────── */ /* ─── Init ────────────────────────────────────────────────────────── */
settingsLoad(); settingsLoad();
const _boot = getCookie("al_session"); const _boot = getCookie("al_session");
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

+627 -205
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Loft — Admin</title> <title>derez.ai — Admin</title>
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
</head> </head>
<body> <body>
@@ -24,45 +24,11 @@
<rect x="2" y="3" width="20" height="14" rx="2" /> <rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" /> <path d="M8 21h8M12 17v4" />
</svg> </svg>
Agent Loft derez.ai
</div>
<div class="sidebar-section-label">Network</div>
<div class="nav-item active" data-section="dns">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
<path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3" />
</svg>
DNS Records
</div>
<div class="nav-item" data-section="coming-soon-firewall">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
Firewall
</div> </div>
<div class="sidebar-section-label">Infrastructure</div> <div class="sidebar-section-label">Infrastructure</div>
<div class="nav-item" data-section="instances"> <div class="nav-item active" data-section="instances">
<svg <svg
width="16" width="16"
height="16" height="16"
@@ -81,6 +47,23 @@
</svg> </svg>
Instances Instances
</div> </div>
<div class="nav-item" data-section="dns">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
<path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3" />
</svg>
DNS Records
</div>
<div class="nav-item" data-section="backup"> <div class="nav-item" data-section="backup">
<svg <svg
width="16" width="16"
@@ -98,6 +81,70 @@
</svg> </svg>
Backup Backup
</div> </div>
<div class="nav-item" data-section="keys">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
/>
</svg>
Keys
</div>
<div
class="sidebar-section-label"
style="
margin-top: 28px;
padding-top: 12px;
border-top: 1px solid var(--border);
"
>
Dashboards
</div>
<div class="nav-item" data-section="crm">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
CRM
</div>
<div class="nav-item" data-section="sales">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
Sales
</div>
<div class="nav-item" data-section="servers"> <div class="nav-item" data-section="servers">
<svg <svg
width="16" width="16"
@@ -116,41 +163,6 @@
</svg> </svg>
Servers Servers
</div> </div>
<div class="nav-item" data-section="coming-soon-ssl">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="11" width="18" height="11" rx="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
SSL / TLS
</div>
<div class="sidebar-section-label">Commercial</div>
<div class="nav-item" data-section="keys">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
/>
</svg>
Keys
</div>
<div class="sidebar-footer"> <div class="sidebar-footer">
<div <div
@@ -182,24 +194,6 @@
</svg> </svg>
<span id="sidebar-username">Not signed in</span> <span id="sidebar-username">Not signed in</span>
</div> </div>
<div class="nav-item" data-section="settings">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.07 4.93a10 10 0 0 0-14.14 0M4.93 19.07a10 10 0 0 0 14.14 0M12 2v2M12 20v2M2 12h2M20 12h2"
/>
</svg>
Settings
</div>
<div <div
class="nav-item" class="nav-item"
onclick="doSignOut()" onclick="doSignOut()"
@@ -265,7 +259,7 @@
<!-- Content --> <!-- Content -->
<div class="content"> <div class="content">
<!-- ─── DNS Section ───────────────────────────────────────── --> <!-- ─── DNS Section ───────────────────────────────────────── -->
<div class="section active" id="section-dns"> <div class="section" id="section-dns">
<!-- Filter / search bar --> <!-- Filter / search bar -->
<div <div
class="card" class="card"
@@ -483,7 +477,7 @@
</div> </div>
<!-- ─── Instances Section ──────────────────────────────────── --> <!-- ─── Instances Section ──────────────────────────────────── -->
<div class="section" id="section-instances"> <div class="section active" id="section-instances">
<!-- Search bar --> <!-- Search bar -->
<div <div
class="card" class="card"
@@ -551,18 +545,17 @@
<tr> <tr>
<th>UUID</th> <th>UUID</th>
<th>Agent</th> <th>Agent</th>
<th>Domain</th> <th>Product</th>
<th>SSH Port</th>
<th>RAM</th>
<th>CPU</th>
<th>Server</th> <th>Server</th>
<th>Created</th> <th>Email</th>
<th>Period</th>
<th>Status</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="inst-table-body"> <tbody id="inst-table-body">
<tr> <tr>
<td colspan="9"> <td colspan="8">
<div class="empty-state"> <div class="empty-state">
<div class="spinner"></div> <div class="spinner"></div>
<p style="margin-top: 12px"> <p style="margin-top: 12px">
@@ -665,67 +658,6 @@
</div> </div>
</div> </div>
<!-- ─── Settings Section ──────────────────────────────────── -->
<div class="section" id="section-settings">
<div class="card">
<div class="card-header">
<span class="card-title">API Connection</span>
</div>
<div class="settings-grid">
<div
class="form-group"
style="grid-column: 1/-1"
>
<label>Webhook Base URL</label>
<input
type="text"
id="setting-url"
placeholder="https://…"
/>
</div>
</div>
<div style="margin-top: 16px">
<button
class="btn btn-primary"
onclick="settingsSave()"
>
Save URL
</button>
<button
class="btn btn-ghost"
onclick="settingsTest()"
style="margin-left: 8px"
>
Test Connection
</button>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Current Session</span>
</div>
<p
style="
font-size: 13px;
color: var(--text-muted);
margin-bottom: 16px;
"
>
Signed in as
<strong id="settings-session-email"></strong>.
Sign out to switch accounts.
</p>
<div style="margin-top: 16px">
<button
class="btn btn-danger"
onclick="doSignOut()"
>
Sign Out
</button>
</div>
</div>
</div>
<!-- ─── Keys Section ───────────────────────────────────── --> <!-- ─── Keys Section ───────────────────────────────────── -->
<div class="section" id="section-keys"> <div class="section" id="section-keys">
<div class="card"> <div class="card">
@@ -785,25 +717,380 @@
</div> </div>
</div> </div>
<!-- ─── Coming-soon placeholders ─────────────────────────── --> <!-- ─── CRM Section ─────────────────────────── -->
<div class="section" id="section-coming-soon-firewall"> <div class="section" id="section-crm">
<!-- Action bar -->
<div
class="card"
style="padding: 10px 20px; margin-bottom: 16px"
>
<div
class="form-row"
style="
flex-wrap: wrap;
gap: 8px;
align-items: center;
"
>
<div class="form-group" style="flex: 0 0 auto">
<button
class="btn btn-sm"
id="crm-hold-btn"
onclick="crmSetStageSelected('Hold')"
disabled
style="
white-space: nowrap;
background: var(--accent-dim);
color: var(--accent);
border: 1px solid var(--border-hi);
"
>
Set Hold
</button>
</div>
<div class="form-group" style="flex: 0 0 auto">
<button
class="btn btn-sm"
id="crm-active-btn"
onclick="crmSetStageSelected('Active')"
disabled
style="
white-space: nowrap;
background: var(--accent-dim);
color: var(--accent);
border: 1px solid var(--border-hi);
"
>
Set Active
</button>
</div>
<div class="form-group" style="flex: 0 0 auto">
<button
class="btn btn-danger btn-sm"
id="crm-delete-selected-btn"
onclick="crmDeleteSelected()"
disabled
style="white-space: nowrap"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="3 6 5 6 21 6" />
<path
d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"
/>
<path d="M10 11v6M14 11v6" />
<path
d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"
/>
</svg>
Delete Checked
</button>
</div>
<div
class="form-group"
style="
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 4px;
"
id="crm-category-counts"
></div>
<span style="flex: 1; min-width: 0"></span>
</div>
</div>
<div class="card"> <div class="card">
<div class="empty-state"> <div class="card-header">
<svg <span class="card-title">
width="48" <svg
height="48" width="16"
viewBox="0 0 24 24" height="16"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" fill="none"
stroke-width="1.5" stroke="currentColor"
stroke-linecap="round" stroke-width="2"
stroke-linejoin="round" stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
/>
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
CRM Contacts
</span>
<span
id="crm-record-count"
class="text-muted"
style="font-size: 12px"
></span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width: 36px">
<input
type="checkbox"
id="crm-check-all"
onchange="crmToggleAll()"
title="Select / deselect all"
/>
</th>
<th
onclick="crmToggleSort('Name')"
style="
cursor: pointer;
user-select: none;
"
>
Name
<span
class="sort-arrow"
data-sort="Name"
></span>
</th>
<th
onclick="
crmToggleSort('Company')
"
style="
cursor: pointer;
user-select: none;
"
>
Company
<span
class="sort-arrow"
data-sort="Company"
></span>
</th>
<th
onclick="crmToggleSort('email')"
style="
cursor: pointer;
user-select: none;
"
>
Email
<span
class="sort-arrow"
data-sort="email"
></span>
</th>
<th
onclick="crmToggleSort('Stage')"
style="
cursor: pointer;
user-select: none;
"
>
Stage
<span
class="sort-arrow"
data-sort="Stage"
></span>
</th>
<th
onclick="
crmToggleSort('strategy')
"
style="
cursor: pointer;
user-select: none;
"
>
Strategy
<span
class="sort-arrow"
data-sort="strategy"
></span>
</th>
<th
onclick="
crmToggleSort('category')
"
style="
cursor: pointer;
user-select: none;
"
>
Category
<span
class="sort-arrow"
data-sort="category"
></span>
</th>
<th
onclick="
crmToggleSort('affiliate')
"
style="
cursor: pointer;
user-select: none;
"
>
Referral
<span
class="sort-arrow"
data-sort="affiliate"
></span>
</th>
<th
onclick="
crmToggleSort(
'date_next_contact',
)
"
style="
cursor: pointer;
user-select: none;
"
>
Date Next Contact
<span
class="sort-arrow"
data-sort="date_next_contact"
></span>
</th>
</tr>
</thead>
<tr class="crm-search-row">
<th></th>
<th>
<input
type="text"
id="crm-search-name"
placeholder="Filter Name…"
oninput="crmRender()"
class="crm-filter-input"
/>
</th>
<th>
<input
type="text"
id="crm-search-company"
placeholder="Filter Company…"
oninput="crmRender()"
class="crm-filter-input"
/>
</th>
<th>
<input
type="text"
id="crm-search-email"
placeholder="Filter Email…"
oninput="crmRender()"
class="crm-filter-input"
/>
</th>
<th>
<input
type="text"
id="crm-search-stage"
placeholder="Filter Stage…"
oninput="crmRender()"
class="crm-filter-input"
/>
</th>
<th>
<input
type="text"
id="crm-search-strategy"
placeholder="Filter Strategy…"
oninput="crmRender()"
class="crm-filter-input"
/>
</th>
<th>
<input
type="text"
id="crm-search-category"
placeholder="Filter Category…"
oninput="crmRender()"
class="crm-filter-input"
/>
</th>
<th style="white-space: nowrap">
<input
type="text"
id="crm-search-affiliate"
placeholder="Filter Referral…"
oninput="crmRender()"
class="crm-filter-input"
/>
<button
class="btn btn-ghost btn-sm"
onclick="crmClearFilters()"
title="Clear all filters"
>
</button>
</th>
</tr>
<tbody id="crm-table-body">
<tr>
<td colspan="9">
<div class="empty-state">
<div class="spinner"></div>
<p style="margin-top: 12px">
Loading contacts…
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ─── Sales Dashboard ─────────────────────────── -->
<div class="section" id="section-sales">
<div class="card">
<div class="card-header">
<span class="card-title">Sales Dashboard</span>
</div>
<div style="padding: 16px 0 0">
<iframe
src="https://derez.ai/operations.html"
style="
width: 100%;
height: 80vh;
border: none;
border-radius: 6px;
background: #fff;
"
title="Sales Dashboard"
allow="clipboard-read; clipboard-write"
></iframe>
<p
style="
font-size: 12px;
color: var(--text-muted);
margin-top: 10px;
text-align: center;
"
> >
<path <a
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" href="https://derez.ai/operations.html"
/> target="_blank"
</svg> style="color: var(--accent)"
<p>Firewall management — coming soon</p> >
Open in new tab ↗
</a>
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -817,33 +1104,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="section" id="section-coming-soon-ssl">
<div class="card">
<div class="empty-state">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="3"
y="11"
width="18"
height="11"
rx="2"
/>
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
<p>SSL / TLS management — coming soon</p>
</div>
</div>
</div>
</div> </div>
<!-- /content --> <!-- /content -->
</div> </div>
@@ -982,6 +1242,168 @@
</div> </div>
</div> </div>
<!-- ═══ CRM EDIT MODAL ════════════════════════════════════ -->
<div class="modal-backdrop" id="crm-edit-backdrop">
<div class="modal" style="max-width: 640px">
<div class="modal-header">
<span class="modal-title" id="crm-edit-title"
>Edit Contact</span
>
<button
class="modal-close"
onclick="crmCloseEdit()"
aria-label="Close"
>
&#x2715;
</button>
</div>
<div class="modal-body">
<input type="hidden" id="crm-edit-id" />
<div class="form-row" style="flex-wrap: wrap">
<div
class="form-group"
style="flex: 1; min-width: 160px"
>
<label>Name</label>
<input
type="text"
id="crm-edit-name"
placeholder="Contact name"
/>
</div>
<div
class="form-group"
style="flex: 1; min-width: 160px"
>
<label>Company</label>
<input
type="text"
id="crm-edit-company"
placeholder="Company"
/>
</div>
<div
class="form-group"
style="flex: 1; min-width: 200px"
>
<label>Email</label>
<input
type="email"
id="crm-edit-email"
placeholder="email@example.com"
/>
</div>
</div>
<div class="form-row" style="flex-wrap: wrap">
<div
class="form-group"
style="flex: 1; min-width: 120px"
>
<label>Stage</label>
<select id="crm-edit-stage">
<option value="">— Select —</option>
<option>Active</option>
<option>Hold</option>
<option>Blacklist</option>
<option>Won</option>
<option>Lost</option>
<option>Contacted</option>
<option>Cold</option>
<option>Warm</option>
<option>Bounced</option>
</select>
</div>
<div
class="form-group"
style="flex: 1; min-width: 160px"
>
<label>Strategy</label>
<input
type="text"
id="crm-edit-strategy"
placeholder="Strategy"
/>
</div>
<div
class="form-group"
style="flex: 1; min-width: 120px"
>
<label>Category</label>
<input
type="text"
id="crm-edit-category"
placeholder="Category"
/>
</div>
</div>
<div class="form-row" style="flex-wrap: wrap">
<div
class="form-group"
style="flex: 1; min-width: 120px"
>
<label>Affiliate</label>
<input
type="text"
id="crm-edit-affiliate"
placeholder="Affiliate"
/>
</div>
<div
class="form-group"
style="flex: 1; min-width: 120px"
>
<label>Number</label>
<input
type="text"
id="crm-edit-number"
placeholder="Phone number"
/>
</div>
<div
class="form-group"
style="flex: 1; min-width: 160px"
>
<label>Date Next Contact</label>
<input type="date" id="crm-edit-date-next" />
</div>
</div>
<div class="form-group" style="margin-top: 10px">
<label>History / Notes</label>
<textarea
id="crm-edit-history"
rows="4"
placeholder="Contact history, notes…"
style="width: 100%; resize: vertical"
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="crmCloseEdit()">
Cancel
</button>
<button
class="btn btn-primary"
id="crm-save-btn"
onclick="crmSaveEdit()"
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
Save Changes
</button>
</div>
</div>
</div>
<!-- ═══ CONFIRM DIALOG ══════════════════════════════════════════════ --> <!-- ═══ CONFIRM DIALOG ══════════════════════════════════════════════ -->
<div class="modal-backdrop" id="confirm-backdrop"> <div class="modal-backdrop" id="confirm-backdrop">
<div class="modal" id="confirm-modal" style="max-width: 360px"> <div class="modal" id="confirm-modal" style="max-width: 360px">
@@ -1026,8 +1448,8 @@
<rect x="2" y="3" width="20" height="14" rx="2" /> <rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" /> <path d="M8 21h8M12 17v4" />
</svg> </svg>
<h2>Agent Loft Admin</h2> <h2>derez.ai Admin</h2>
<p>Sign in with your Agent Loft account</p> <p>Sign in with your derez.ai account</p>
</div> </div>
<div id="login-error"></div> <div id="login-error"></div>
<div class="modal-body"> <div class="modal-body">
+118 -73
View File
@@ -8,29 +8,33 @@
} }
:root { :root {
--bg: #0f1117; --bg: #000810;
--bg-card: #1a1d27; --bg-card: #010f20;
--bg-hover: #22263a; --bg-inner: #000d1a;
--bg-input: #12151f; --bg-hover: #001a30;
--border: #2a2f45; --bg-input: #000813;
--accent: #4f8ef7; --border: #0a3060;
--accent-dim: #2a4a8a; --border-hi: rgba(0, 245, 255, 0.45);
--danger: #e05252; --accent: #00f5ff;
--danger-dim: #5a1f1f; --accent-dim: #002535;
--success: #3fc97e; --danger: #ff3b3b;
--warning: #f0a04b; --danger-dim: #3b0000;
--text: #e4e8f5; --success: #00ff88;
--text-muted: #7a82a0; --warning: #ffaa00;
--text: #c8f0ff;
--text-muted: #2e6a80;
--sidebar-w: 230px; --sidebar-w: 230px;
--header-h: 54px; --header-h: 54px;
--radius: 8px; --radius: 8px;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.45); --shadow:
0 0 0 1px rgba(0, 245, 255, 0.25), 0 0 60px rgba(0, 245, 255, 0.12),
0 28px 72px rgba(0, 0, 0, 0.9);
} }
html, html,
body { body {
height: 100%; height: 100%;
font-family: "Segoe UI", system-ui, sans-serif; font-family: "Courier New", Courier, monospace;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
font-size: 14px; font-size: 14px;
@@ -63,7 +67,7 @@ body {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
font-weight: 700; font-weight: 700;
font-size: 15px; font-size: 15px;
letter-spacing: 0.3px; letter-spacing: 1.5px;
color: var(--accent); color: var(--accent);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -127,17 +131,23 @@ body {
.topbar { .topbar {
height: var(--header-h); height: var(--header-h);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border-hi);
background: linear-gradient(
90deg,
rgba(0, 245, 255, 0.08),
transparent 60%
);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 28px; padding: 0 22px;
flex-shrink: 0; flex-shrink: 0;
gap: 14px; gap: 14px;
} }
.topbar-title { .topbar-title {
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: var(--text);
} }
.topbar-actions { .topbar-actions {
display: flex; display: flex;
@@ -161,10 +171,10 @@ body {
/* ─── Cards ─────────────────────────────────────────────────── */ /* ─── Cards ─────────────────────────────────────────────────── */
.card { .card {
background: var(--bg-card); background: var(--bg-inner);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
padding: 20px 24px; padding: 16px 18px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.card-header { .card-header {
@@ -176,11 +186,12 @@ body {
flex-wrap: wrap; flex-wrap: wrap;
} }
.card-title { .card-title {
font-size: 15px; font-size: 13px;
font-weight: 600; font-weight: 600;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 7px;
color: var(--text);
} }
/* ─── Buttons ───────────────────────────────────────────────── */ /* ─── Buttons ───────────────────────────────────────────────── */
@@ -206,8 +217,19 @@ body {
opacity: 0.7; opacity: 0.7;
} }
.btn-primary { .btn-primary {
background: var(--accent); background: transparent;
color: #fff; color: #fff;
border: 1px solid var(--accent);
box-shadow:
0 0 8px rgba(0, 245, 255, 0.35),
inset 0 0 8px rgba(0, 245, 255, 0.04);
}
.btn-primary:hover {
background: rgba(0, 245, 255, 0.08);
box-shadow:
0 0 14px rgba(0, 245, 255, 0.5),
inset 0 0 10px rgba(0, 245, 255, 0.08);
opacity: 1;
} }
.btn-danger { .btn-danger {
background: var(--danger); background: var(--danger);
@@ -223,7 +245,7 @@ body {
color: #000; color: #000;
} }
.btn-sm { .btn-sm {
padding: 4px 10px; padding: 5px 10px;
font-size: 12px; font-size: 12px;
} }
.btn:disabled { .btn:disabled {
@@ -257,7 +279,7 @@ body {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
color: var(--text); color: var(--text);
padding: 8px 10px; padding: 9px 11px;
font-size: 13px; font-size: 13px;
outline: none; outline: none;
transition: border-color 0.15s; transition: border-color 0.15s;
@@ -315,44 +337,44 @@ td:last-child {
font-weight: 600; font-weight: 600;
} }
.badge-a { .badge-a {
background: #1a3a5c; background: #001a2e;
color: #5fb3f5; color: #00c8e0;
} }
.badge-aaaa { .badge-aaaa {
background: #1a2f4c; background: #001225;
color: #8ac8f8; color: #50b8d8;
} }
.badge-cname { .badge-cname {
background: #2a1f4a; background: #0e0028;
color: #a888f8; color: #9060f0;
} }
.badge-mx { .badge-mx {
background: #1f3a2a; background: #001a0e;
color: #5fd89a; color: #00cc60;
} }
.badge-txt { .badge-txt {
background: #3a2a1a; background: #1a1000;
color: #f0b060; color: #cc8800;
} }
.badge-ns { .badge-ns {
background: #3a1a2a; background: #200012;
color: #f080b0; color: #cc5088;
} }
.badge-other { .badge-other {
background: #2a2a2a; background: #080c12;
color: #9090a0; color: #4a6880;
} }
.badge-eu { .badge-eu {
background: #1a2f4c; background: #001225;
color: #8ac8f8; color: #50b8d8;
} }
.badge-us { .badge-us {
background: #1f3a2a; background: #001a0e;
color: #5fd89a; color: #00cc60;
} }
.badge-region { .badge-region {
background: #2a2a2a; background: #080c12;
color: #9090a0; color: #4a6880;
} }
.status-dot { .status-dot {
@@ -431,7 +453,7 @@ td:last-child {
} }
.modal { .modal {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border); border: 1px solid var(--border-hi);
border-radius: 10px; border-radius: 10px;
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
@@ -484,6 +506,7 @@ td:last-child {
/* ─── Confirm dialog ────────────────────────────────────────── */ /* ─── Confirm dialog ────────────────────────────────────────── */
#confirm-backdrop .modal { #confirm-backdrop .modal {
max-width: 380px; max-width: 380px;
border-radius: 12px;
} }
/* ─── Login modal ───────────────────────────────────────────── */ /* ─── Login modal ───────────────────────────────────────────── */
@@ -503,7 +526,7 @@ td:last-child {
} }
#login-modal { #login-modal {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border); border: 1px solid var(--border-hi);
border-radius: 12px; border-radius: 12px;
width: 100%; width: 100%;
max-width: 360px; max-width: 360px;
@@ -518,13 +541,13 @@ td:last-child {
} }
.login-logo h2 { .login-logo h2 {
margin-top: 10px; margin-top: 10px;
font-size: 17px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: var(--accent); color: var(--text);
} }
.login-logo p { .login-logo p {
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 13px;
color: var(--text-muted); color: var(--text-muted);
} }
#login-error { #login-error {
@@ -587,8 +610,8 @@ td:last-child {
gap: 8px; gap: 8px;
} }
.mono { .mono {
font-family: "Cascadia Code", "Fira Code", monospace; font-family: "Courier New", Courier, monospace;
font-size: 12px; font-size: 11.5px;
} }
.text-muted { .text-muted {
color: var(--text-muted); color: var(--text-muted);
@@ -611,7 +634,7 @@ hr.sep {
.server-tab { .server-tab {
padding: 8px 20px 10px; padding: 8px 20px 10px;
cursor: pointer; cursor: pointer;
border: 1px solid transparent; border: 1px solid var(--border);
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
color: var(--text-muted); color: var(--text-muted);
@@ -624,6 +647,7 @@ hr.sep {
position: relative; position: relative;
bottom: -2px; bottom: -2px;
user-select: none; user-select: none;
background: transparent;
transition: transition:
background 0.12s, background 0.12s,
color 0.12s, color 0.12s,
@@ -632,14 +656,14 @@ hr.sep {
.server-tab:hover { .server-tab:hover {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text); color: var(--text);
border-color: var(--border); border-color: rgba(0, 245, 255, 0.4);
border-bottom-color: var(--bg-hover); border-bottom-color: var(--bg-hover);
} }
.server-tab.active { .server-tab.active {
background: var(--bg); background: var(--accent-dim);
border-color: var(--border); border-color: var(--accent);
border-bottom-color: var(--bg); border-bottom-color: var(--accent-dim);
color: var(--text); color: var(--accent);
} }
.server-tab-dot { .server-tab-dot {
display: inline-block; display: inline-block;
@@ -683,14 +707,14 @@ hr.sep {
.prog-wrap { .prog-wrap {
background: var(--bg-input); background: var(--bg-input);
border-radius: 4px; border-radius: 4px;
height: 6px; height: 4px;
margin-top: 6px; margin-top: 6px;
overflow: hidden; overflow: hidden;
} }
.prog-bar { .prog-bar {
height: 100%; height: 100%;
border-radius: 4px; border-radius: 4px;
transition: width 0.4s ease; transition: width 0.5s ease;
min-width: 2px; min-width: 2px;
} }
.prog-low { .prog-low {
@@ -735,28 +759,28 @@ hr.sep {
/* ─── Status / risk badges (dashboard) ──────────────────────── */ /* ─── Status / risk badges (dashboard) ──────────────────────── */
.badge-green { .badge-green {
background: #1c3a26; background: rgba(0, 255, 136, 0.1);
color: #5fd89a; color: var(--success);
} }
.badge-yellow { .badge-yellow {
background: #372900; background: rgba(255, 170, 0, 0.1);
color: #f0b060; color: var(--warning);
} }
.badge-red { .badge-red {
background: #3a1515; background: rgba(255, 59, 59, 0.12);
color: #f07070; color: var(--danger);
} }
.badge-low { .badge-low {
background: #1c3a26; background: rgba(0, 255, 136, 0.1);
color: #5fd89a; color: var(--success);
} }
.badge-medium { .badge-medium {
background: #372900; background: rgba(255, 170, 0, 0.1);
color: #f0b060; color: var(--warning);
} }
.badge-high { .badge-high {
background: #3a1515; background: rgba(255, 59, 59, 0.12);
color: #f07070; color: var(--danger);
} }
/* ─── Action advice items ─────────────────────────────────── */ /* ─── Action advice items ─────────────────────────────────── */
@@ -795,3 +819,24 @@ hr.sep {
font-size: 13px; font-size: 13px;
padding: 3px 0; padding: 3px 0;
} }
/* ─── CRM filter inputs ──────────────────────────────────── */
.crm-filter-input {
width: 100%;
box-sizing: border-box;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text);
border-radius: 4px;
padding: 4px 6px;
font-size: 12px;
font-family: inherit;
}
.crm-filter-input:focus {
outline: none;
border-color: var(--border-hi);
}
.crm-search-row th {
padding: 6px 4px;
vertical-align: middle;
}