Files
simplex-manager/manager/templates/base.html
Jon 34469455a4 Restore Get SimpleX App link in the footer
It still appears as a homepage card; keep the footer link too (removing it
earlier was not requested).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:25:58 +01:00

363 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<script>
(function(){
var t=localStorage.getItem('theme');
if(!t){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'original-dark':'original-light';}
document.documentElement.setAttribute('data-theme',t);
if(localStorage.getItem('sidebar-collapsed')) document.documentElement.classList.add('collapsed');
})();
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}SimpleX Manager{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
<script>
document.addEventListener('htmx:configRequest', function(evt) {
const m = document.cookie.match(/(?:^|;\s*)token=([^;]+)/);
if (m) evt.detail.headers['X-Token'] = m[1];
});
</script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Original Light ─────────────────────────────────────────────── */
[data-theme="original-light"] {
--bg: #f5f5f7;
--card: #ffffff;
--text: #1d1d1f;
--muted: #6e6e73;
--accent: #0053D0;
--green: #20BD3D;
--red: #DD0000;
--border: #e0e0e5;
--shadow: 0 2px 12px rgba(0,0,0,0.08);
--btn-light-text: #fff;
--badge-green-bg: #d1fae5;
--badge-green-text: #065f46;
--badge-red-bg: #fee2e2;
--badge-red-text: #991b1b;
}
/* ── Original Dark ──────────────────────────────────────────────── */
[data-theme="original-dark"] {
--bg: #111827;
--card: #0B2A59;
--text: #f5f5f7;
--muted: #9ca3af;
--accent: #70F0F9;
--green: #20BD3D;
--red: #DD0000;
--border: #1e3a5f;
--shadow: none;
--btn-light-text: #000;
--badge-green-bg: #064e3b;
--badge-green-text: #6ee7b7;
--badge-red-bg: #7f1d1d;
--badge-red-text: #fca5a5;
}
/* ── Matrix ─────────────────────────────────────────────────────── */
[data-theme="matrix"] {
--bg: #000000;
--card: #050d05;
--text: #00ff41;
--muted: #2e8b57;
--accent: #00ff41;
--green: #00ff41;
--red: #ff3b3b;
--border: #0f3d0f;
--shadow: 0 0 14px rgba(0,255,65,0.12);
--btn-light-text: #000000;
--badge-green-bg: #002200;
--badge-green-text: #00ff41;
--badge-red-bg: #220000;
--badge-red-text: #ff6b6b;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: var(--bg); color: var(--text); min-height: 100vh; }
[data-theme="matrix"] body {
font-family: 'SF Mono', 'Consolas', 'Courier New', monospace;
text-shadow: 0 0 2px rgba(0,255,65,0.4);
}
/* ── Layout: sidebar + main ─────────────────────────────────────── */
.app { display: flex; min-height: 100vh; }
.sidebar {
width: 220px; flex-shrink: 0;
background: var(--card); border-right: 1px solid var(--border);
display: flex; flex-direction: column;
position: sticky; top: 0; height: 100vh;
overflow-y: auto; /* scroll within the sidebar if it's taller than the screen */
transition: width 0.2s ease, transform 0.2s ease;
z-index: 50;
}
html.collapsed .sidebar { width: 64px; }
.nav-brand {
display: flex; align-items: center; gap: 10px;
padding: 18px; font-size: 16px; font-weight: 700;
color: var(--accent); text-decoration: none;
border-bottom: 1px solid var(--border); white-space: nowrap; overflow: hidden;
}
.nav-brand .brand-icon { font-size: 18px; flex-shrink: 0; }
.side-nav { display: flex; flex-direction: column; padding: 8px 0; }
.side-nav a {
position: relative;
display: flex; align-items: center; gap: 12px;
padding: 11px 18px; color: var(--muted); text-decoration: none;
font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden;
border-left: 3px solid transparent;
}
.notif-badge {
margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px;
border-radius: 9px; background: var(--red); color: #fff;
font-size: 11px; font-weight: 700;
display: inline-flex; align-items: center; justify-content: center;
}
html.collapsed .notif-badge {
position: absolute; top: 5px; right: 8px; margin: 0;
min-width: 16px; height: 16px; font-size: 10px; padding: 0 4px;
}
.side-nav a:hover { color: var(--text); background: var(--bg); }
.side-nav a.active { color: var(--accent); border-left-color: var(--accent); }
.side-nav .ico { width: 20px; text-align: center; font-size: 16px; flex-shrink: 0; }
.side-nav a.nav-sep { margin-top: 10px; padding-top: 17px; border-top: 1px solid var(--border); }
.side-foot { margin-top: auto; padding: 8px 0; border-top: 1px solid var(--border); }
.side-status { padding: 10px 18px 12px; font-size: 12px; color: var(--muted);
border-bottom: 1px solid var(--border); }
.side-status .ss-title { font-weight: 700; text-transform: uppercase; font-size: 10px;
letter-spacing: 0.5px; margin-bottom: 7px; opacity: 0.7; }
.side-status .ss-row { display: flex; align-items: center; gap: 6px; margin-top: 3px;
white-space: nowrap; overflow: hidden; }
.ss-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex-shrink: 0; }
.ss-dot.online { background: var(--green); box-shadow: 0 0 5px var(--green); }
.ss-dot.offline { background: var(--red); }
html.collapsed .side-status { display: none; }
.collapse-btn {
display: flex; align-items: center; justify-content: space-between; gap: 10px;
margin: 8px 12px; padding: 8px 12px; border-radius: 8px;
background: var(--bg); border: 1px solid var(--border); cursor: pointer;
color: var(--muted); font-family: inherit; font-size: 12px; font-weight: 600;
white-space: nowrap; overflow: hidden;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.collapse-btn:hover { color: var(--text); border-color: var(--accent); }
.collapse-btn .ico { font-size: 16px; line-height: 1; flex-shrink: 0; }
html.collapsed .collapse-btn { justify-content: center; margin: 8px; padding: 8px; }
html.collapsed .lbl, html.collapsed .brand-text { display: none; }
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.container { max-width: 960px; margin: 0 auto; padding: 32px 20px; width: 100%; flex: 1 0 auto; }
.site-footer {
flex-shrink: 0; text-align: center;
padding: 18px 20px; border-top: 1px solid var(--border);
color: var(--muted); font-size: 12px; line-height: 1.6;
}
.site-footer a { color: var(--accent); text-decoration: none; font-weight: 600; }
.site-footer a:hover { text-decoration: underline; }
.site-footer .sep { margin: 0 8px; opacity: 0.5; }
.mobile-menu-btn {
display: none; position: fixed; top: 12px; left: 12px; z-index: 40;
width: 40px; height: 40px; border-radius: 8px; border: 1px solid var(--border);
background: var(--card); color: var(--text); font-size: 18px; cursor: pointer;
align-items: center; justify-content: center;
}
.backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 45; }
/* ── Mobile: off-canvas sidebar ─────────────────────────────────── */
@media (max-width: 768px) {
.sidebar {
position: fixed; left: 0; top: 0; width: 240px;
height: 100vh; height: 100dvh; /* dvh tracks the visible area incl. browser toolbar */
overflow-y: auto; -webkit-overflow-scrolling: touch;
transform: translateX(-100%);
}
html.collapsed .sidebar { width: 240px; } /* ignore collapse on mobile */
html.collapsed .lbl, html.collapsed .brand-text { display: inline; }
body.sidebar-open .sidebar { transform: translateX(0); }
body.sidebar-open .backdrop { display: block; }
.mobile-menu-btn { display: flex; }
.collapse-btn { display: none; }
.container { padding-top: 64px; }
}
h1 { font-size: 28px; font-weight: 700; margin-bottom: 24px; }
h2 { font-size: 20px; font-weight: 600; margin-bottom: 16px; }
.card { background: var(--card); border-radius: 10px; padding: 20px;
box-shadow: var(--shadow); margin-bottom: 16px; }
.btn { display: inline-flex; align-items: center; gap: 6px;
padding: 8px 18px; border-radius: 8px; font-size: 14px; font-weight: 600;
font-family: inherit; cursor: pointer; border: none; text-decoration: none;
transition: opacity 0.15s; }
.btn:hover { opacity: 0.85; }
.btn-primary { background: var(--accent); color: var(--btn-light-text); }
.btn-danger { background: var(--red); color: #fff; }
.btn-success { background: var(--green); color: var(--btn-light-text); }
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px;
font-size: 12px; font-weight: 600; }
.badge-green { background: var(--badge-green-bg); color: var(--badge-green-text); }
.badge-red { background: var(--badge-red-bg); color: var(--badge-red-text); }
input, select, textarea {
width: 100%; padding: 9px 12px; font-size: 14px; font-family: inherit;
border: 1px solid var(--border); border-radius: 8px;
background: var(--bg); color: var(--text); outline: none;
transition: border-color 0.15s;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
label { display: block; font-size: 13px; font-weight: 600;
color: var(--muted); margin-bottom: 4px; }
.field { margin-bottom: 14px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 640px) { .grid-2 { grid-template-columns: 1fr; } }
.monospace { font-family: monospace; font-size: 12px; }
.log-box { background: #0a0a0f; color: #70F0F9; border-radius: 8px;
padding: 12px; font-family: monospace; font-size: 12px;
height: 200px; overflow-y: auto; white-space: pre-wrap; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 6px;
font-size: 12px; background: var(--border); color: var(--muted); }
.tag-user { background: rgba(0,83,208,0.12); color: var(--accent); }
[data-theme="original-dark"] .tag-user { background: rgba(112,240,249,0.12); color: var(--accent); }
[data-theme="matrix"] .tag-user { background: rgba(0,255,65,0.12); color: var(--accent); }
.flex { display: flex; align-items: center; gap: 10px; }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
.gap-8 { gap: 8px; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.muted { color: var(--muted); font-size: 13px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { text-align: left; color: var(--muted); font-size: 12px; font-weight: 600;
padding: 8px 12px; border-bottom: 1px solid var(--border); }
td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
.htmx-indicator { opacity: 0; transition: opacity 0.2s; }
.htmx-request .htmx-indicator { opacity: 1; }
dialog { background: var(--card); color: var(--text); border: 1px solid var(--border);
border-radius: 12px; padding: 28px; max-width: 480px; width: 90%; }
dialog::backdrop { background: rgba(0,0,0,0.5); }
</style>
{% block head %}{% endblock %}
</head>
<body>
<button class="mobile-menu-btn" onclick="toggleSidebar()" aria-label="Menu"></button>
<div class="app">
<aside class="sidebar" id="sidebar">
<a class="nav-brand" href="/">
<span class="brand-icon"></span><span class="brand-text">SimpleX Manager</span>
</a>
<nav class="side-nav">
<a href="/users" {% if nav_active == 'users' %}class="active"{% endif %}><span class="ico">👤</span><span class="lbl">Users</span></a>
<a href="/businesses" {% if nav_active == 'businesses' %}class="active"{% endif %}><span class="ico">💼</span><span class="lbl">Businesses</span></a>
<a href="/bots" {% if nav_active == 'bots' %}class="active"{% endif %}><span class="ico">🤖</span><span class="lbl">Bots</span></a>
<a href="https://simplex.chat/file/" target="_blank" rel="noopener"><span class="ico">📁</span><span class="lbl">File upload</span></a>
<a href="/notifications" class="nav-sep {% if nav_active == 'notifications' %}active{% endif %}"><span class="ico">🔔</span><span class="lbl">Notifications</span><span class="notif-badge" id="notif-badge" style="display:none;"></span></a>
<a href="/settings" class="nav-sep {% if nav_active == 'settings' %}active{% endif %}"><span class="ico">⚙️</span><span class="lbl">Settings</span></a>
</nav>
<div class="side-foot">
<a href="/network" class="side-status" id="side-status" title="View SimpleX network &amp; servers"
style="display:block;text-decoration:none;{% if nav_active == 'network' %}background:var(--bg);{% endif %}">
<div class="ss-title">Network </div>
<div class="ss-row"><span class="ss-dot" id="ss-dot"></span><span id="ss-running">/</span>&nbsp;running</div>
<div class="ss-row" id="ss-servers">📡 </div>
<div class="ss-row" id="ss-ops" style="opacity:0.8;"></div>
</a>
<button class="collapse-btn" onclick="toggleCollapse()" title="Collapse sidebar" aria-label="Collapse sidebar">
<span class="lbl">Collapse</span><span class="ico" id="collapse-ico"></span>
</button>
<nav class="side-nav">
<a href="/logout"><span class="ico"></span><span class="lbl">Logout</span></a>
</nav>
</div>
</aside>
<div class="backdrop" id="backdrop" onclick="closeSidebar()"></div>
<main class="main">
<div class="container">
{% block content %}{% endblock %}
</div>
<footer class="site-footer">
© <a href="https://bournemouthtechnology.co.uk" target="_blank" rel="noopener">Bournemouth Technology Ltd</a>
<span class="sep">·</span>
built on © <a href="https://simplex.chat" target="_blank" rel="noopener">SimpleX Network</a>
<span class="sep">·</span>
<a href="https://simplex.chat/downloads/" target="_blank" rel="noopener">Get SimpleX App</a>
</footer>
</main>
</div>
<script>
function toggleSidebar() { document.body.classList.toggle('sidebar-open'); }
function closeSidebar() { document.body.classList.remove('sidebar-open'); }
function toggleCollapse() {
const collapsed = document.documentElement.classList.toggle('collapsed');
localStorage.setItem('sidebar-collapsed', collapsed ? '1' : '');
const ico = document.getElementById('collapse-ico');
if (ico) ico.textContent = collapsed ? '' : '';
}
// Sync collapse icon with restored state on load
(function(){
const ico = document.getElementById('collapse-ico');
if (ico && document.documentElement.classList.contains('collapsed')) ico.textContent = '';
})();
// Poll for unread notifications and update the sidebar badge
async function pollNotifications() {
try {
const t = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
const r = await fetch('/api/notifications', { headers: { 'X-Token': t } });
if (!r.ok) return;
const d = await r.json();
const b = document.getElementById('notif-badge');
if (!b) return;
if (d.unread > 0) { b.textContent = d.unread > 99 ? '99+' : d.unread; b.style.display = 'inline-flex'; }
else { b.style.display = 'none'; }
} catch (e) {}
}
pollNotifications();
setInterval(pollNotifications, 5000);
// Poll global SimpleX/network status for the sidebar widget
async function pollStatus() {
try {
const t = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
const r = await fetch('/api/status', { headers: { 'X-Token': t } });
if (!r.ok) return;
const d = await r.json();
document.getElementById('ss-running').textContent = d.profiles_running + '/' + d.profiles_total;
const dot = document.getElementById('ss-dot');
dot.className = 'ss-dot ' + (d.online ? 'online' : 'offline');
document.getElementById('ss-servers').textContent =
d.online ? `📡 ${d.smp_servers} SMP · ${d.xftp_servers} XFTP` : 'no profile running';
document.getElementById('ss-ops').textContent =
(d.operators && d.operators.length) ? d.operators.join(', ') : '';
} catch (e) {}
}
pollStatus();
setInterval(pollStatus, 15000);
</script>
</body>
</html>