Split the home tiles into three areas with vertical spacing: accounts (Users/Businesses/Bots), manage (Network/Notifications/Settings), and external SimpleX (File upload + a new Get SimpleX App card). Move the Get SimpleX App link out of the global footer into its own homepage card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
361 lines
17 KiB
HTML
361 lines
17 KiB
HTML
<!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 & 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> 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>
|
||
</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>
|