Files
simplex-manager/manager/templates/base.html
Jon 270766b99b Add tiled homepage; rename Business→Businesses; link footer copyright
- Home page (home.html) at / shows the sidebar sections as tiles; reachable by
  clicking the 'SimpleX Manager' brand in the sidebar (was redirecting to /users).
- Rename the category to 'Businesses' (route /businesses, tab/nav/_category),
  keeping the per-account bot_type 'business'. Fix profile back-link label.
- Footer: link 'Bournemouth Technology Ltd' -> bournemouthtechnology.co.uk and
  'SimpleX Network' -> simplex.chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:45:28 +01:00

360 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; gap: 12px; width: 100%;
padding: 11px 18px; background: none; border: none; cursor: pointer;
color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 600;
white-space: nowrap; overflow: hidden;
}
.collapse-btn:hover { color: var(--text); }
.collapse-btn .ico { width: 20px; text-align: center; flex-shrink: 0; }
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="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>