openvibers/openvibe.network Commits

Status words and badges use the readable status twins (var(--<status>-text, var(--<status>))) (#306)

OP OpenVibers committed 22 days ago · cff515e40b · 14 files, +67 −67

Changes

ddiff --git a/public/admin.html b/public/admin.html
iindex df7ce611..2c5bd0dd 100644
---- a/public/admin.html
++++ b/public/admin.html
@@@ -335,7 +335,7 @@
<div id="analytics-overview">
<div class="stats-grid" id="analytics-stats"><div class="loading">Loading analytics...</div></div>
<div class="card" id="analytics-realtime-card" style="display:none">
-- <h3><i class="fa-solid fa-signal" style="color:var(--success)"></i> Real-time (last 5 min)</h3>
++ <h3><i class="fa-solid fa-signal" style="color:var(--success-text, var(--success))"></i> Real-time (last 5 min)</h3>
<div id="analytics-realtime"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
ddiff --git a/public/css/admin.css b/public/css/admin.css
iindex 037683e6..6696878b 100644
---- a/public/css/admin.css
++++ b/public/css/admin.css
@@@ -69,9 +69,9 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
.btn-primary:hover{background:var(--accent-dark)}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}
--.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}
++.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red-text, var(--live-red));border:1px solid rgba(231,76,60,0.2)}
.btn-danger:hover{background:rgba(231,76,60,0.2)}
--.btn-success{background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.2)}
++.btn-success{background:rgba(46,204,113,0.1);color:var(--success-text, var(--success));border:1px solid rgba(46,204,113,0.2)}
.btn-success:hover{background:rgba(46,204,113,0.2)}
.btn-sm{padding:5px 10px;font-size:11px}
a.btn{text-decoration:none}
@@@ -156,5 +156,5 @@ a.btn{text-decoration:none}
}
.ssh-prompt{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:8px;padding:14px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;line-height:1.55;color:var(--text,#ddd);max-height:520px;overflow:auto;margin:0}
#sec-ssh code,#sec-payments code,#sec-cashouts code{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:5px;padding:2px 7px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;display:inline-block;margin-top:3px;word-break:break-all}
--#sec-ssh .staff-pill{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:999px;vertical-align:middle;margin-left:8px;color:var(--warning,#f0b74a);background:color-mix(in srgb,var(--warning,#f0b74a) 18%,transparent);border:1px solid color-mix(in srgb,var(--warning,#f0b74a) 40%,transparent)}
++#sec-ssh .staff-pill{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:999px;vertical-align:middle;margin-left:8px;color:var(--warning-text, var(--warning,#f0b74a));background:color-mix(in srgb,var(--warning,#f0b74a) 18%,transparent);border:1px solid color-mix(in srgb,var(--warning,#f0b74a) 40%,transparent)}
#sec-ssh .staff-pill.user-pill{color:var(--accent,#6ea8ff);background:color-mix(in srgb,var(--accent,#6ea8ff) 18%,transparent);border-color:color-mix(in srgb,var(--accent,#6ea8ff) 40%,transparent)}
ddiff --git a/public/css/my.css b/public/css/my.css
iindex 5a5f4e8b..4dcd01d9 100644
---- a/public/css/my.css
++++ b/public/css/my.css
@@@ -90,7 +90,7 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
.btn-primary:hover{background:var(--accent-dark)}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}
--.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}
++.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red-text, var(--live-red));border:1px solid rgba(231,76,60,0.2)}
.btn-danger:hover{background:rgba(231,76,60,0.2)}
.btn-row{display:flex;gap:8px;margin-top:12px}
.data-check{display:flex;gap:8px;align-items:flex-start;font-size:13px;color:var(--text-secondary);line-height:1.5}
@@@ -147,12 +147,12 @@ a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translat
.hist-item .hx{margin-left:auto;display:flex;align-items:center;gap:10px;flex:none;font-size:11px;color:var(--text-muted)}
.hist-item .hx button{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:4px;opacity:0;transition:opacity .15s}
.hist-item:hover .hx button{opacity:1}
--.hist-item .hx button:hover{color:var(--danger,#ef4444)}
++.hist-item .hx button:hover{color:var(--danger-text, var(--danger,#ef4444))}
.hist-empty{padding:26px;text-align:center;color:var(--text-muted);font-size:13px}
.linked-item .svc-icon{font-size:20px;width:32px;text-align:center}
.linked-item .svc-name{font-size:13px;font-weight:600}
.linked-item .svc-user{font-size:11px;color:var(--text-muted)}
--.linked-item .svc-status{margin-left:auto;font-size:11px;font-weight:600;color:var(--success)}
++.linked-item .svc-status{margin-left:auto;font-size:11px;font-weight:600;color:var(--success-text, var(--success))}
/* Themes */
.theme-hero{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:18px}
ddiff --git a/public/js/admin/ai.js b/public/js/admin/ai.js
iindex a5d517bc..7d3362a7 100644
---- a/public/js/admin/ai.js
++++ b/public/js/admin/ai.js
@@@ -232,12 +232,12 @@ async function loadStreamerMediaTools() {
<h3><i class="fa-solid fa-wrench"></i> OpenVibe.Live yt-dlp Status</h3>
<div style="display:grid;gap:8px;font-size:0.95rem">
<div><strong>yt-dlp available:</strong> ${status.ytdlp_available
-- ? '<span style="color:var(--success,#22c55e)"><i class="fa-solid fa-check-circle"></i> Yes</span>'
-- : '<span style="color:var(--danger,#ef4444)"><i class="fa-solid fa-xmark-circle"></i> No</span>'
++ ? '<span style="color:var(--success-text, var(--success,#22c55e))"><i class="fa-solid fa-check-circle"></i> Yes</span>'
++ : '<span style="color:var(--danger-text, var(--danger,#ef4444))"><i class="fa-solid fa-xmark-circle"></i> No</span>'
}</div>
<div><strong>Path:</strong> <span style="font-family:monospace;font-size:0.88rem">${esc(status.ytdlp_path || '')}</span></div>
<div><strong>YouTube cookies:</strong> ${status.cookies_configured
-- ? `<span style="color:var(--success,#22c55e)"><i class="fa-solid fa-cookie"></i> Configured (${esc(String(status.cookies_size || 0))} bytes)</span>`
++ ? `<span style="color:var(--success-text, var(--success,#22c55e))"><i class="fa-solid fa-cookie"></i> Configured (${esc(String(status.cookies_size || 0))} bytes)</span>`
: '<span class="muted"><i class="fa-solid fa-cookie-bite"></i> Not configured</span>'
}</div>
${!status.cookies_configured ? '<div style="color:var(--warn,#f59e0b);font-size:0.88rem"><i class="fa-solid fa-triangle-exclamation"></i> YouTube server-side downloads will fail until cookies.txt is added.</div>' : ''}
@@@ -317,8 +317,8 @@ function renderCookieVerdict(data) {
const c = data.check || {};
const t = data.test || {};
const ok = (v) => v
-- ? '<span style="color:var(--success,#22c55e)"><i class="fa-solid fa-circle-check"></i> '
-- : '<span style="color:var(--danger,#ef4444)"><i class="fa-solid fa-circle-xmark"></i> ';
++ ? '<span style="color:var(--success-text, var(--success,#22c55e))"><i class="fa-solid fa-circle-check"></i> '
++ : '<span style="color:var(--danger-text, var(--danger,#ef4444))"><i class="fa-solid fa-circle-xmark"></i> ';
const rows = [];
rows.push(`<div>${ok(c.ok)}Signed-in cookies${c.ok ? '' : ' — missing'}</span></div>`);
@@@ -338,7 +338,7 @@ function renderCookieVerdict(data) {
rows.push(`<div style="margin-top:6px">${ok(t.ok)}Live extraction${t.ok ? `: ${esc(t.title || 'ok')}` : ' failed'}</span></div>`);
if (!t.ok && t.error) rows.push(`<div class="muted" style="font-size:.82rem;word-break:break-word">${esc(String(t.error).slice(0, 300))}</div>`);
}
-- (c.errors || []).forEach(e => rows.push(`<div style="color:var(--danger,#ef4444);font-size:.86rem;margin-top:6px">${esc(e)}</div>`));
++ (c.errors || []).forEach(e => rows.push(`<div style="color:var(--danger-text, var(--danger,#ef4444));font-size:.86rem;margin-top:6px">${esc(e)}</div>`));
(c.warnings || []).forEach(w => rows.push(`<div style="color:var(--warn,#f59e0b);font-size:.86rem">${esc(w)}</div>`));
el.innerHTML = `<div style="margin-top:12px;padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-input)">${rows.join('')}</div>`;
@@@ -351,7 +351,7 @@ async function checkStreamerMediaCookies() {
const data = await api('/api/admin/streamer/media-tools/cookies/check', { method: 'POST', body: {} });
renderCookieVerdict(data);
} catch (e) {
-- if (el) el.innerHTML = `<div style="color:var(--danger,#ef4444);margin-top:10px">${esc(e.message)}</div>`;
++ if (el) el.innerHTML = `<div style="color:var(--danger-text, var(--danger,#ef4444));margin-top:10px">${esc(e.message)}</div>`;
}
}
@@@ -369,7 +369,7 @@ async function saveStreamerMediaCookies() {
// The server refuses a jar it can already tell is unusable and returns the reason.
toast(e.message, 'error');
if (e.body) renderCookieVerdict(e.body);
-- else if (el) el.innerHTML = `<div style="color:var(--danger,#ef4444);margin-top:10px">${esc(e.message)}</div>`;
++ else if (el) el.innerHTML = `<div style="color:var(--danger-text, var(--danger,#ef4444));margin-top:10px">${esc(e.message)}</div>`;
}
}
@@@ -391,15 +391,15 @@ async function testStreamerMediaExtraction() {
const data = await api('/api/admin/streamer/media-tools/test', { method: 'POST', body: { url } });
results.innerHTML = `<div style="display:grid;gap:8px;font-size:0.9rem">${(data.steps || []).map(step => {
const icon = step.ok
-- ? '<i class="fa-solid fa-check-circle" style="color:var(--success,#22c55e)"></i>'
-- : '<i class="fa-solid fa-xmark-circle" style="color:var(--danger,#ef4444)"></i>';
++ ? '<i class="fa-solid fa-check-circle" style="color:var(--success-text, var(--success,#22c55e))"></i>'
++ : '<i class="fa-solid fa-xmark-circle" style="color:var(--danger-text, var(--danger,#ef4444))"></i>';
const detail = step.error
-- ? `<span style="color:var(--danger,#ef4444)"> — ${esc(step.error)}</span>`
++ ? `<span style="color:var(--danger-text, var(--danger,#ef4444))"> — ${esc(step.error)}</span>`
: step.data ? ` — ${esc(JSON.stringify(step.data))}` : '';
return `<div>${icon} <strong>${esc(step.name)}</strong>${detail}</div>`;
}).join('')}</div>`;
} catch (e) {
-- results.innerHTML = `<p style="color:var(--danger,#ef4444)">${esc(e.message)}</p>`;
++ results.innerHTML = `<p style="color:var(--danger-text, var(--danger,#ef4444))">${esc(e.message)}</p>`;
}
}
ddiff --git a/public/js/admin/analytics.js b/public/js/admin/analytics.js
iindex 4b30c959..464961fc 100644
---- a/public/js/admin/analytics.js
++++ b/public/js/admin/analytics.js
@@@ -125,8 +125,8 @@ async function loadAnalyticsOverview() {
document.getElementById('analytics-realtime').innerHTML = `
<div style="display:flex;gap:24px;font-size:14px">
<span><strong style="color:var(--accent-light)">${fmtNum(rt.requests)}</strong> requests</span>
-- <span><strong style="color:var(--success)">${fmtNum(rt.visitors)}</strong> visitors</span>
-- <span><strong style="color:var(--danger)">${fmtNum(rt.bots)}</strong> bots</span>
++ <span><strong style="color:var(--success-text, var(--success))">${fmtNum(rt.visitors)}</strong> visitors</span>
++ <span><strong style="color:var(--danger-text, var(--danger))">${fmtNum(rt.bots)}</strong> bots</span>
</div>`;
} else {
rtCard.style.display = 'none';
@@@ -309,10 +309,10 @@ async function loadAnalyticsOverview() {
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:13px">
<div><strong style="color:var(--accent-light)">${fmtNum(bw.total_requests)}</strong><br><span class="muted">Total Requests</span></div>
<div><strong style="color:var(--accent-light)">${fmtBytes(bw.estimated_bytes)}</strong><br><span class="muted">Est. Transfer</span></div>
-- <div><strong style="color:var(--info)">${fmtNum(bw.page_requests)}</strong><br><span class="muted">Page Loads</span></div>
-- <div><strong style="color:var(--info)">${fmtNum(bw.api_requests)}</strong><br><span class="muted">API Requests</span></div>
-- <div><strong style="color:var(--success)">${fmtNum(ov.sessionCount || 0)}</strong><br><span class="muted">Unique Sessions</span></div>
-- <div><strong style="color:var(--success)">${fmtNum(t.total_unique_users || 0)}</strong><br><span class="muted">Logged-in Users</span></div>
++ <div><strong style="color:var(--info-text, var(--info))">${fmtNum(bw.page_requests)}</strong><br><span class="muted">Page Loads</span></div>
++ <div><strong style="color:var(--info-text, var(--info))">${fmtNum(bw.api_requests)}</strong><br><span class="muted">API Requests</span></div>
++ <div><strong style="color:var(--success-text, var(--success))">${fmtNum(ov.sessionCount || 0)}</strong><br><span class="muted">Unique Sessions</span></div>
++ <div><strong style="color:var(--success-text, var(--success))">${fmtNum(t.total_unique_users || 0)}</strong><br><span class="muted">Logged-in Users</span></div>
</div>`;
} catch (e) {
@@@ -478,11 +478,11 @@ async function loadServiceAnalytics(serviceName) {
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;font-size:13px">
<div><strong style="color:var(--accent-light)">${fmtBytes(bw.estimated_bytes)}</strong><br><span class="muted">Est. Transfer</span></div>
<div><strong style="color:var(--accent-light)">${fmtNum(bw.total_requests)}</strong><br><span class="muted">Total Requests</span></div>
-- <div><strong style="color:var(--info)">${fmtNum(bw.page_requests)}</strong><br><span class="muted">Page Loads</span></div>
-- <div><strong style="color:var(--info)">${fmtNum(bw.api_requests)}</strong><br><span class="muted">API Requests</span></div>
-- <div><strong style="color:var(--success)">${fmtNum(a.sessionCount || 0)}</strong><br><span class="muted">Sessions</span></div>
++ <div><strong style="color:var(--info-text, var(--info))">${fmtNum(bw.page_requests)}</strong><br><span class="muted">Page Loads</span></div>
++ <div><strong style="color:var(--info-text, var(--info))">${fmtNum(bw.api_requests)}</strong><br><span class="muted">API Requests</span></div>
++ <div><strong style="color:var(--success-text, var(--success))">${fmtNum(a.sessionCount || 0)}</strong><br><span class="muted">Sessions</span></div>
${vis.new_visitors != null
-- ? `<div><strong style="color:var(--success)">${fmtNum(vis.new_visitors)}</strong> new / <strong>${fmtNum(vis.returning_visitors)}</strong> returning<br><span class="muted">Visitors</span></div>`
++ ? `<div><strong style="color:var(--success-text, var(--success))">${fmtNum(vis.new_visitors)}</strong> new / <strong>${fmtNum(vis.returning_visitors)}</strong> returning<br><span class="muted">Visitors</span></div>`
: '<div><strong class="muted">Not measured</strong><br><span class="muted" title="ADR-021: no visitor identity outlives a day">New / returning</span></div>'}
</div>`;
@@@ -546,7 +546,7 @@ async function loadServiceAnalytics(serviceName) {
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(e.path)}">${esc(e.path)}</td>
<td><span class="badge badge-warning">${esc(e.method)}</span></td>
<td style="color:${e.avg_ms > 500 ? 'var(--danger)' : e.avg_ms > 200 ? 'var(--warning)' : 'inherit'}">${e.avg_ms || 0}ms</td>
-- <td style="color:var(--danger)">${e.max_ms || 0}ms</td>
++ <td style="color:var(--danger-text, var(--danger))">${e.max_ms || 0}ms</td>
<td>${fmtNum(e.hits)}</td>
</tr>
`).join('')}</tbody>
@@@ -624,7 +624,7 @@ async function loadBotAnalysis() {
<table class="admin-table"><thead><tr><th>Session</th><th>Total Hits</th><th>Errors</th><th>Unique Paths</th><th>Period</th></tr></thead>
<tbody>${suspicious.map(s => `
<tr><td style="font-family:monospace;font-size:11px">${esc(s.session_id || '-')}</td><td>${fmtNum(s.total_hits)}</td>
-- <td style="color:var(--danger)">${fmtNum(s.error_hits)}</td><td>${fmtNum(s.unique_paths)}</td>
++ <td style="color:var(--danger-text, var(--danger))">${fmtNum(s.error_hits)}</td><td>${fmtNum(s.unique_paths)}</td>
<td style="font-size:11px">${s.first_seen ? timeAgo(s.first_seen) : '-'} — ${s.last_seen ? timeAgo(s.last_seen) : '-'}</td></tr>
`).join('')}</tbody></table>`;
}
ddiff --git a/public/js/admin/boot.js b/public/js/admin/boot.js
iindex fb3247ed..1a1e4f62 100644
---- a/public/js/admin/boot.js
++++ b/public/js/admin/boot.js
@@@ -61,7 +61,7 @@ async function _bootAdmin() {
const data = await api('/api/auth/me');
currentUser = data.user || data;
if (currentUser.role !== 'admin') {
-- document.querySelector('.page').innerHTML = '<div class="card" style="text-align:center;padding:60px"><h3 style="color:var(--live-red)"><i class="fa-solid fa-lock"></i> Access Denied</h3><p class="muted" style="margin-top:8px">Admin privileges required.</p></div>';
++ document.querySelector('.page').innerHTML = '<div class="card" style="text-align:center;padding:60px"><h3 style="color:var(--live-red-text, var(--live-red))"><i class="fa-solid fa-lock"></i> Access Denied</h3><p class="muted" style="margin-top:8px">Admin privileges required.</p></div>';
return;
}
// Owner-only UI (Deploy tab, etc.) — hide for plain admins.
ddiff --git a/public/js/admin/content.js b/public/js/admin/content.js
iindex cdc7622c..9ff1e03a 100644
---- a/public/js/admin/content.js
++++ b/public/js/admin/content.js
@@@ -154,8 +154,8 @@ function buildStreamerTierHtml(tierData) {
if (!p) return '<span style="color:var(--text-muted)">—</span>';
if (!p.configured) return '<span style="color:var(--text-muted)">not configured</span>';
return p.healthy
-- ? '<span style="color:var(--success,#22c55e)"><i class="fa-solid fa-circle-check"></i> healthy</span>' + (p.bucket ? ' <span style="color:var(--text-muted);font-size:11px">· ' + esc(p.bucket) + '</span>' : '')
-- : '<span style="color:var(--danger,#ef4444)"><i class="fa-solid fa-circle-xmark"></i> error</span>';
++ ? '<span style="color:var(--success-text, var(--success,#22c55e))"><i class="fa-solid fa-circle-check"></i> healthy</span>' + (p.bucket ? ' <span style="color:var(--text-muted);font-size:11px">· ' + esc(p.bucket) + '</span>' : '')
++ : '<span style="color:var(--danger-text, var(--danger,#ef4444))"><i class="fa-solid fa-circle-xmark"></i> error</span>';
};
const rows = [
{ label: 'Local (SSD)', icon: 'fa-hard-drive', color: '#f59e0b', v: L, clips: (ct.local && ct.local.count) || 0, status: '<span style="color:var(--text-muted)">primary disk</span>' },
@@@ -212,7 +212,7 @@ async function loadStreamerCloudStorage(force) {
const u = usage[key];
const cc = costs[key];
if (!u || u.configured === false) return '<div class="card" style="flex:1;min-width:260px"><strong style="color:' + color + '"><i class="fa-solid fa-cloud"></i> ' + label + '</strong><div style="color:var(--text-muted);margin-top:8px">Not configured</div></div>';
-- if (u.error) return '<div class="card" style="flex:1;min-width:260px"><strong style="color:' + color + '"><i class="fa-solid fa-cloud"></i> ' + label + '</strong><div style="color:var(--danger,#ef4444);margin-top:8px">Scan error: ' + esc(u.error) + '</div></div>';
++ if (u.error) return '<div class="card" style="flex:1;min-width:260px"><strong style="color:' + color + '"><i class="fa-solid fa-cloud"></i> ' + label + '</strong><div style="color:var(--danger-text, var(--danger,#ef4444));margin-top:8px">Scan error: ' + esc(u.error) + '</div></div>';
const tops = Object.entries(u.prefixes || {}).sort((a, b) => b[1].bytes - a[1].bytes).slice(0, 8);
const prefixRows = tops.map(([p, v]) => '<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted);padding:2px 0"><span>' + esc(p) + '</span><span>' + fmtBytes(v.bytes) + ' · ' + v.objects.toLocaleString() + '</span></div>').join('');
return '<div class="card" style="flex:1;min-width:260px">'
@@@ -226,7 +226,7 @@ async function loadStreamerCloudStorage(force) {
+ '<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px">' + card('b2', 'Backblaze B2', '#e21e2b') + card('r2', 'Cloudflare R2', '#f6821f') + '</div>'
+ '<div class="card" style="display:flex;align-items:center;justify-content:space-between"><span><i class="fa-solid fa-file-invoice-dollar" style="color:var(--accent)"></i> <strong>Estimated cloud storage cost</strong> <span style="color:var(--text-muted);font-size:12px">(list prices; excludes egress/operations)</span></span><span style="font-size:22px;font-weight:800;color:var(--accent)">' + money(costs.totalStorageMonthly) + '<span style="font-size:13px;font-weight:400;color:var(--text-muted)">/mo</span></span></div>';
} catch (e) {
-- el.innerHTML = header + '<div style="color:var(--danger,#ef4444)">Failed to scan buckets: ' + esc(e.message) + '</div>';
++ el.innerHTML = header + '<div style="color:var(--danger-text, var(--danger,#ef4444))">Failed to scan buckets: ' + esc(e.message) + '</div>';
}
}
ddiff --git a/public/js/admin/dashboard.js b/public/js/admin/dashboard.js
iindex 8c804e1b..e34f33ab 100644
---- a/public/js/admin/dashboard.js
++++ b/public/js/admin/dashboard.js
@@@ -49,7 +49,7 @@ async function loadDashboard() {
<div><strong>Heap Used:</strong> ${fmtBytes(mem?.heapUsed)}</div>
<div><strong>Heap Total:</strong> ${fmtBytes(mem?.heapTotal)}</div>
<div><strong>RSS:</strong> ${fmtBytes(mem?.rss)}</div>
-- <div><strong>Email:</strong> ${th.ses_enabled ? '<span style="color:var(--success)">Enabled</span>' : '<span style="color:var(--text-muted)">Disabled</span>'}</div>
++ <div><strong>Email:</strong> ${th.ses_enabled ? '<span style="color:var(--success-text, var(--success))">Enabled</span>' : '<span style="color:var(--text-muted)">Disabled</span>'}</div>
</div>
`;
} else {
ddiff --git a/public/js/admin/deploy.js b/public/js/admin/deploy.js
iindex 89ff7443..6ea9b23e 100644
---- a/public/js/admin/deploy.js
++++ b/public/js/admin/deploy.js
@@@ -92,7 +92,7 @@ function sshCopy(id, btn) {
// ── Owner view ──────────────────────────────────────────────
function renderSSHOwner(el) {
const hostWarn = sshInfo.host ? '' :
-- `<div class="card" style="border-color:var(--warning)"><p style="color:var(--warning);margin:0"><i class="fa-solid fa-triangle-exclamation"></i> No server host is configured yet. Set <code>SSH_SERVER_HOST</code> in the openvibe-network environment so the connection details fill in automatically.</p></div>`;
++ `<div class="card" style="border-color:var(--warning)"><p style="color:var(--warning-text, var(--warning));margin:0"><i class="fa-solid fa-triangle-exclamation"></i> No server host is configured yet. Set <code>SSH_SERVER_HOST</code> in the openvibe-network environment so the connection details fill in automatically.</p></div>`;
el.innerHTML = `
<div class="card">
<h3><i class="fa-solid fa-user-lock"></i> Provision an SSH dev account <span class="staff-pill">Owner</span></h3>
@@@ -116,7 +116,7 @@ function renderSSHOwner(el) {
<option value="web">Full web + nginx — build &amp; run their own sites (high trust)</option>
</select>
</div>
-- <div id="ssh-o-webwarn" style="display:none;margin-top:12px;padding:10px 12px;border:1px solid var(--warning);border-radius:8px;background:color-mix(in srgb,var(--warning) 10%,transparent);color:var(--warning);font-size:0.84rem;line-height:1.5">
++ <div id="ssh-o-webwarn" style="display:none;margin-top:12px;padding:10px 12px;border:1px solid var(--warning);border-radius:8px;background:color-mix(in srgb,var(--warning) 10%,transparent);color:var(--warning-text, var(--warning));font-size:0.84rem;line-height:1.5">
<i class="fa-solid fa-triangle-exclamation"></i> <b>High trust:</b> web + nginx access lets this admin edit any nginx site config and reload the web server — they can serve anything and a bad config can take <em>all</em> the sites down. It still stops short of root (no package installs or arbitrary sudo), but only grant it to admins you fully trust to run the web layer with you.
</div>
</div>
@@@ -330,12 +330,12 @@ async function loadDeployOverview() {
items.push(prereqLine('ACME email configured', !!c.acmeEmail, c.acmeEmail || 'Not set'));
prereqsEl.innerHTML = items.join('');
} catch (e) {
-- document.getElementById('deploy-stats').innerHTML = `<div class="muted" style="color:var(--danger)">${esc(e.message)}</div>`;
++ document.getElementById('deploy-stats').innerHTML = `<div class="muted" style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
function prereqLine(label, ok, detail) {
-- const icon = ok ? '<i class="fa-solid fa-circle-check" style="color:var(--success)"></i>' : '<i class="fa-solid fa-circle-xmark" style="color:var(--danger)"></i>';
++ const icon = ok ? '<i class="fa-solid fa-circle-check" style="color:var(--success-text, var(--success))"></i>' : '<i class="fa-solid fa-circle-xmark" style="color:var(--danger-text, var(--danger))"></i>';
const detailHtml = detail ? ` <span class="muted" style="font-size:11px">— ${esc(detail)}</span>` : '';
return `<div style="padding:6px 0;display:flex;align-items:center;gap:8px">${icon} ${esc(label)}${detailHtml}</div>`;
}
@@@ -368,7 +368,7 @@ async function loadDeployCerts() {
</div>`;
}).join('');
} catch (e) {
-- el.innerHTML = `<div class="muted" style="color:var(--danger)">${esc(e.message)}</div>`;
++ el.innerHTML = `<div class="muted" style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -384,15 +384,15 @@ async function issueCertCloudflare() {
});
const d = await r.json();
if (d.ok) {
-- out.innerHTML = `<div style="color:var(--success);padding:8px"><i class="fa-solid fa-check-circle"></i> Certificate issued: ${esc(d.certName || domain)}</div>
++ out.innerHTML = `<div style="color:var(--success-text, var(--success));padding:8px"><i class="fa-solid fa-check-circle"></i> Certificate issued: ${esc(d.certName || domain)}</div>
<pre style="background:var(--bg-input);padding:8px;border-radius:6px;font-size:11px;max-height:200px;overflow:auto;margin-top:8px">${esc(d.output || '')}</pre>`;
loadDeployCerts();
} else {
-- out.innerHTML = `<div style="color:var(--danger);padding:8px"><i class="fa-solid fa-xmark"></i> ${esc(d.error || 'Failed')}</div>
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger));padding:8px"><i class="fa-solid fa-xmark"></i> ${esc(d.error || 'Failed')}</div>
<pre style="background:var(--bg-input);padding:8px;border-radius:6px;font-size:11px;max-height:200px;overflow:auto;margin-top:8px">${esc(d.output || '')}</pre>`;
}
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -419,7 +419,7 @@ async function showManualDnsInfo() {
</div>
</div>`;
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -433,15 +433,15 @@ async function issueManualCert(domain) {
});
const d = await r.json();
if (d.ok) {
-- out.innerHTML = `<div style="color:var(--success);padding:8px"><i class="fa-solid fa-check-circle"></i> Certificate issued: ${esc(d.certName || domain)}</div>
++ out.innerHTML = `<div style="color:var(--success-text, var(--success));padding:8px"><i class="fa-solid fa-check-circle"></i> Certificate issued: ${esc(d.certName || domain)}</div>
<pre style="background:var(--bg-input);padding:8px;border-radius:6px;font-size:11px;max-height:200px;overflow:auto;margin-top:8px">${esc(d.output || '')}</pre>`;
loadDeployCerts();
} else {
-- out.innerHTML = `<div style="color:var(--danger);padding:8px"><i class="fa-solid fa-xmark"></i> ${esc(d.error || 'Failed')}</div>
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger));padding:8px"><i class="fa-solid fa-xmark"></i> ${esc(d.error || 'Failed')}</div>
<pre style="background:var(--bg-input);padding:8px;border-radius:6px;font-size:11px;max-height:200px;overflow:auto;margin-top:8px">${esc(d.output || '')}</pre>`;
}
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -459,7 +459,7 @@ async function renewAllCerts() {
<pre style="background:var(--bg-input);padding:8px;border-radius:6px;font-size:11px;max-height:200px;overflow:auto;margin-top:8px">${esc(d.output || '')}</pre>`;
if (d.ok) loadDeployCerts();
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -480,7 +480,7 @@ async function previewNginxConfigs() {
</div>
`).join('');
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -500,7 +500,7 @@ async function validateNginx() {
<pre style="background:var(--bg-input);padding:8px;border-radius:6px;font-size:11px;margin-top:8px;max-height:200px;overflow:auto">${esc(d.output || '')}</pre>
</div>`;
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -521,10 +521,10 @@ async function applyNginxConfigs() {
if (d.backed_up?.length) lines.push(`<div class="muted">Backed up: ${d.backed_up.length} files</div>`);
if (d.validation) lines.push(`<div>Validation: ${d.validation.ok ? '✓ passed' : '✗ failed'}</div>`);
if (d.reload) lines.push(`<div>Reload: ${d.reload.ok ? '✓ success' : '✗ failed'}</div>`);
-- if (d.errors?.length) lines.push(`<div style="color:var(--danger)">Errors: ${d.errors.map(e => esc(e)).join('; ')}</div>`);
++ if (d.errors?.length) lines.push(`<div style="color:var(--danger-text, var(--danger))">Errors: ${d.errors.map(e => esc(e)).join('; ')}</div>`);
out.innerHTML = `<div class="card" style="padding:16px">${lines.join('')}</div>`;
} catch (e) {
-- out.innerHTML = `<div style="color:var(--danger)">${esc(e.message)}</div>`;
++ out.innerHTML = `<div style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -555,7 +555,7 @@ async function loadDeployDomains() {
</div>
`).join('');
} catch (e) {
-- el.innerHTML = `<div class="muted" style="color:var(--danger)">${esc(e.message)}</div>`;
++ el.innerHTML = `<div class="muted" style="color:var(--danger-text, var(--danger))">${esc(e.message)}</div>`;
}
}
@@@ -601,7 +601,7 @@ async function loadDeployConfig() {
document.getElementById('deploy-nginx-mode').value = d.config.nginxMode || 'preview';
document.getElementById('deploy-nginx-path').value = d.config.nginxSitesPath || '/etc/nginx/sites-enabled';
} catch (e) {
-- document.getElementById('deploy-config-status').innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`;
++ document.getElementById('deploy-config-status').innerHTML = `<span style="color:var(--danger-text, var(--danger))">${esc(e.message)}</span>`;
}
}
@@@ -621,9 +621,9 @@ async function saveDeployConfig() {
});
const d = await r.json();
if (!d.ok) throw new Error(d.error);
-- status.innerHTML = '<span style="color:var(--success)"><i class="fa-solid fa-check"></i> Saved</span>';
++ status.innerHTML = '<span style="color:var(--success-text, var(--success))"><i class="fa-solid fa-check"></i> Saved</span>';
setTimeout(() => status.innerHTML = '', 3000);
} catch (e) {
-- status.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`;
++ status.innerHTML = `<span style="color:var(--danger-text, var(--danger))">${esc(e.message)}</span>`;
}
}
ddiff --git a/public/js/admin/settings.js b/public/js/admin/settings.js
iindex a796e711..2a497293 100644
---- a/public/js/admin/settings.js
++++ b/public/js/admin/settings.js
@@@ -228,14 +228,14 @@ function _streamerSettingField(s) {
return `<div class="setting-row" style="flex-direction:column;align-items:stretch">${label}
<textarea id="ss-${esc(s.key)}" data-key="${esc(s.key)}" data-type="${kind}" rows="${rows}" spellcheck="false"
style="${base};font-family:ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical;white-space:pre;overflow:auto">${esc(shown)}</textarea>
-- <div class="setting-err muted" data-err-for="${esc(s.key)}" style="display:none;color:var(--danger,#f87171);font-size:12px;margin-top:4px"></div></div>`;
++ <div class="setting-err muted" data-err-for="${esc(s.key)}" style="display:none;color:var(--danger-text, var(--danger,#f87171));font-size:12px;margin-top:4px"></div></div>`;
}
const type = s.type === 'number' ? 'number' : (isSensitive ? 'password' : 'text');
const width = s.type === 'number' ? 'width:200px;' : '';
return `<div class="setting-row" style="flex-direction:column;align-items:stretch">${label}
<input type="${type}" id="ss-${esc(s.key)}" data-key="${esc(s.key)}" data-type="${esc(s.type||'string')}" value="${esc(s.value)}" ${type === 'number' ? 'step="any"' : ''}
style="${base};${width}">
-- <div class="setting-err muted" data-err-for="${esc(s.key)}" style="display:none;color:var(--danger,#f87171);font-size:12px;margin-top:4px"></div></div>`;
++ <div class="setting-err muted" data-err-for="${esc(s.key)}" style="display:none;color:var(--danger-text, var(--danger,#f87171));font-size:12px;margin-top:4px"></div></div>`;
}
// Snapshot of what the server sent (keyed by setting key) so Save only submits real changes.
ddiff --git a/public/js/my/profile.js b/public/js/my/profile.js
iindex 80e3de7f..7ad3843e 100644
---- a/public/js/my/profile.js
++++ b/public/js/my/profile.js
@@@ -223,7 +223,7 @@ function loadAccounts() {
<div class="meta">${a.is_anon ? 'Anonymous #' + (a.anon_number || '?') : '@' + a.username}</div>
</div>
<div class="actions">
-- ${String(a.id) !== String(activeId) ? `<button class="btn btn-outline" onclick="switchAccount('${a.id}')">Switch</button>` : '<span style="font-size:11px;color:var(--success);font-weight:600">Active</span>'}
++ ${String(a.id) !== String(activeId) ? `<button class="btn btn-outline" onclick="switchAccount('${a.id}')">Switch</button>` : '<span style="font-size:11px;color:var(--success-text, var(--success));font-weight:600">Active</span>'}
${String(a.id) !== String(activeId) ? `<button class="btn btn-danger" onclick="removeAccount('${a.id}')"><i class="fa-solid fa-xmark"></i></button>` : ''}
</div>
</div>
ddiff --git a/public/js/my/security.js b/public/js/my/security.js
iindex 767bb8bd..06cbd8ce 100644
---- a/public/js/my/security.js
++++ b/public/js/my/security.js
@@@ -15,9 +15,9 @@ async function loadEmailStatus() {
if (!st.email) {
body.innerHTML = `<p>No email address on your account, so you can't receive go-live emails. Add one in <a href="#" onclick="showSection('profile');return false">Profile</a> — we'll send a confirmation link.</p>`;
} else if (st.bounced) {
-- body.innerHTML = `<p><span style="color:var(--danger,#ef4444)"><i class="fa-solid fa-triangle-exclamation"></i> Mail to <strong>${esc(st.email)}</strong> is bouncing</span>${st.bounce_reason ? ` (${esc(st.bounce_reason)})` : ''}. Emails are paused until you update the address in <a href="#" onclick="showSection('profile');return false">Profile</a> and confirm it.</p>`;
++ body.innerHTML = `<p><span style="color:var(--danger-text, var(--danger,#ef4444))"><i class="fa-solid fa-triangle-exclamation"></i> Mail to <strong>${esc(st.email)}</strong> is bouncing</span>${st.bounce_reason ? ` (${esc(st.bounce_reason)})` : ''}. Emails are paused until you update the address in <a href="#" onclick="showSection('profile');return false">Profile</a> and confirm it.</p>`;
} else if (st.verified) {
-- body.innerHTML = `<p><span style="color:var(--success,#22c55e)"><i class="fa-solid fa-circle-check"></i> <strong>${esc(st.email)}</strong> is verified.</span> You'll get an email when a streamer you follow goes live (turn this off under Streams → Email below).</p>`;
++ body.innerHTML = `<p><span style="color:var(--success-text, var(--success,#22c55e))"><i class="fa-solid fa-circle-check"></i> <strong>${esc(st.email)}</strong> is verified.</span> You'll get an email when a streamer you follow goes live (turn this off under Streams → Email below).</p>`;
} else {
body.innerHTML = `<p><i class="fa-solid fa-envelope"></i> <strong>${esc(st.email)}</strong> isn't confirmed yet — go-live and other opt-in emails stay off until it is.</p>
<button class="btn btn-primary" id="notif-verify-btn" onclick="resendVerification(this)" ${st.can_resend && st.email_enabled ? '' : 'disabled'}><i class="fa-solid fa-paper-plane"></i> ${st.email_enabled ? 'Send verification email' : 'Email delivery not configured'}</button>
@@@ -116,7 +116,7 @@ async function loadLinked() {
const who = l.service_username && !String(l.service_user_id || '').startsWith('network:') ? '@' + l.service_username : (l.service_username ? '@' + l.service_username : 'this account');
const inner = `<span class="svc-icon" style="color:${site.color || 'var(--accent-light)'}"><i class="fa-solid ${site.icon}"></i></span>
<div><div class="svc-name">${esc(site.name)}</div><div class="svc-user">${esc(who)}</div></div>
-- <div class="svc-meta">${l.last_used_at ? 'used ' + ago(l.last_used_at) : (l.linked_at ? 'linked ' + ago(l.linked_at) : '')}<br><span class="svc-status" style="color:var(--success)"><i class="fa-solid fa-check"></i> Linked</span></div>`;
++ <div class="svc-meta">${l.last_used_at ? 'used ' + ago(l.last_used_at) : (l.linked_at ? 'linked ' + ago(l.linked_at) : '')}<br><span class="svc-status" style="color:var(--success-text, var(--success))"><i class="fa-solid fa-check"></i> Linked</span></div>`;
return site.url ? `<a class="linked-item" href="${site.url}" target="_blank" rel="noopener">${inner}</a>` : `<div class="linked-item">${inner}</div>`;
}).join('');
const rest = Object.entries(SITES).filter(([id]) => !known.has(id) && id !== 'network' && id !== 'discord');
ddiff --git a/public/verify-email.html b/public/verify-email.html
iindex 8d881729..c189cc3f 100644
---- a/public/verify-email.html
++++ b/public/verify-email.html
@@@ -32,8 +32,8 @@ body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',
.ring.spin{border-top-color:var(--accent);animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.glyph .ico{position:absolute;inset:0;display:grid;place-items:center;font-size:38px;color:var(--accent-light);transition:transform .35s cubic-bezier(.2,1.4,.3,1),color .3s}
--.card[data-state="ok"] .ico{color:var(--success);transform:scale(1.12)}
--.card[data-state="bad"] .ico{color:var(--danger)}
++.card[data-state="ok"] .ico{color:var(--success-text, var(--success));transform:scale(1.12)}
++.card[data-state="bad"] .ico{color:var(--danger-text, var(--danger))}
.card[data-state="ok"] .ring{border-color:rgba(34,197,94,.35);animation:pop .6s cubic-bezier(.2,1.6,.3,1) both}
.card[data-state="bad"] .ring{border-color:rgba(239,68,68,.35)}
@keyframes pop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.08)}100%{transform:scale(1);opacity:1}}
ddiff --git a/server/status/routes.js b/server/status/routes.js
iindex f17a3f28..cbe2be27 100644
---- a/server/status/routes.js
++++ b/server/status/routes.js
@@@ -141,7 +141,7 @@ const CSS = `
.st th{font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted,#7386a3)}
.st td small{display:block;color:var(--text-muted,#7386a3);font-size:12.5px;margin-top:2px}
.st code{font-size:12.5px;overflow-wrap:anywhere}.st-b{display:inline-block;padding:2px 9px;border-radius:999px;font-weight:700;font-size:12.5px;border:1px solid currentColor}
--.st-up{color:var(--success,#22c55e)}.st-degraded{color:#f59e0b}.st-down{color:var(--live-red,#ef4444)}.st-not-running,.st-unknown{color:var(--text-muted,#7386a3)}
++.st-up{color:var(--success-text, var(--success,#22c55e))}.st-degraded{color:#f59e0b}.st-down{color:var(--live-red-text, var(--live-red,#ef4444))}.st-not-running,.st-unknown{color:var(--text-muted,#7386a3)}
.st ul.chk{margin:4px 0 0;padding:0;list-style:none;font-size:12.5px}.st ul.chk li{margin:1px 0}
.st section{margin-top:40px}.st dl{display:grid;grid-template-columns:minmax(160px,240px) 1fr;gap:6px 16px;font-size:14px}.st dt{font-weight:600}.st dd{margin:0;color:var(--text-secondary,#96a7c2)}
@media (max-width:720px){.st table,.st tbody,.st tr,.st td{display:block}.st thead{display:none}.st tr{border-bottom:1px solid var(--border,#1f2d47);padding:8px 0}.st td{border:0;padding:4px 0}.st dl{grid-template-columns:1fr}}`;