Files
simplex-manager/manager/templates/base.html
Jon ea5efb06d8 Align sidebar + homepage groups; flatten Relays (drop dropdown); move Notifications to system group
Both the sidebar and the homepage now share the same logical groups:
accounts (Users/Businesses/Bots/File Upload), relays (Chat/File/Message Relay),
system (Network/Notifications/Settings), external (Get App). Relays is now its
own flat divider-separated section in the sidebar instead of a collapsible
dropdown; remove the dropdown CSS/JS. Move Notifications into the system group
on the homepage cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:15:46 +01:00

369 lines
18 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-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">
<!-- Group 1: accounts -->
<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>
<!-- Group 2: relays -->
<a href="/relays/chat" class="nav-sep {% if nav_active == 'relays' and kind == 'chat' %}active{% endif %}"><span class="ico">💬</span><span class="lbl">Chat Relay</span></a>
<a href="/relays/file" {% if nav_active == 'relays' and kind == 'file' %}class="active"{% endif %}><span class="ico">📤</span><span class="lbl">File Relay</span></a>
<a href="/relays/message" {% if nav_active == 'relays' and kind == 'message' %}class="active"{% endif %}><span class="ico">✉️</span><span class="lbl">Message Relay</span></a>
<!-- Group 3: system -->
<a href="/network" class="nav-sep {% if nav_active == 'network' %}active{% endif %}"><span class="ico">📡</span><span class="lbl">Network</span></a>
<a href="/notifications" {% if nav_active == 'notifications' %}class="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" {% if nav_active == 'settings' %}class="active"{% endif %}><span class="ico">⚙️</span><span class="lbl">Settings</span></a>
<!-- Group 4: external -->
<a href="https://simplex.chat/downloads/" target="_blank" rel="noopener" class="nav-sep"><span class="ico">📲</span><span class="lbl">Get App</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-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>