Files
simplex-manager/manager/templates/base.html
Jon c1bb9cb955 Add chat actions, channels/groups mgmt, directory + deadmans bots, network status
Profiles/bots (profiles.py, main.py):
- Surface real send errors; fix group send and member-count staleness (refresh on view)
- Group/channel actions: join, leave, owner delete; consistent member counts
- Contacts: always-visible Chat, plus Clear chat and Delete contact
- Support bot: OpenAI-compatible LLM backend (Grok/Ollama/OpenAI) per-bot config
- Deadmans bot: check-in window, trigger message, recipients, owner
- Directory bot: add-to-group registration, super-user /approve /reject /list, search,
  publishes listing.json in the website schema (directory.py registry module)
- Profile edit (name/bio/avatar) + avatars on list pages
- Global status + /network page (operators, SMP/XFTP servers) and Settings network info

UI (templates):
- Chat rooms; collapsible left sidebar with notifications + network widget
- Per-directory-bot website generated on creation (name substituted)
- Matrix theme; copy/hyperlink addresses; site footer

Ignore runtime bot state and generated directory sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:26:16 +01:00

356 lines
16 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;
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; height: 100vh; width: 240px;
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="/users">
<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="/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">
© Bournemouth Technology Ltd
<span class="sep">·</span>
built on © SimpleX Network
<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>