Add chat rooms, channels, sidebar nav, themes, and UI polish
Backend (profiles.py / main.py): - Fix bot startup crash: create active user before start_chat - Fix address-clobbering bug on restart (UserContactLink vs CreatedConnLink) - Add user profile type alongside bots - Channels: create groups with observer links, classify via acceptMemberRole - Chat rooms: get_chat_history + send_to_chat, history/messages/send routes UI: - Chat room view with message bubbles and live polling - Convert top nav to collapsible left sidebar (mobile-friendly off-canvas) - Three-way Users/Bots split; clickable cards; copy-address buttons + links - Add Matrix theme alongside Original Light/Dark - File upload sidebar link; site footer on all pages incl. login - Bot-type descriptions on the Bots page; QR caption on profiles Remove orphaned index.html (superseded by list.html). Ignore downloaded libs/, exploration DBs, and local ai.sh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
<!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>
|
||||
// Inject auth token into every HTMX request automatically
|
||||
document.addEventListener('htmx:configRequest', function(evt) {
|
||||
const m = document.cookie.match(/(?:^|;\s*)token=([^;]+)/);
|
||||
if (m) evt.detail.headers['X-Token'] = m[1];
|
||||
@@ -15,43 +22,147 @@
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--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);
|
||||
/* ── 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;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #111827;
|
||||
--card: #0B2A59;
|
||||
--text: #f5f5f7;
|
||||
--muted: #9ca3af;
|
||||
--accent: #70F0F9;
|
||||
--border: #1e3a5f;
|
||||
--shadow: none;
|
||||
}
|
||||
/* ── 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; }
|
||||
|
||||
nav { background: var(--card); border-bottom: 1px solid var(--border);
|
||||
padding: 12px 24px; display: flex; align-items: center; justify-content: space-between;
|
||||
position: sticky; top: 0; z-index: 10; }
|
||||
[data-theme="matrix"] body {
|
||||
font-family: 'SF Mono', 'Consolas', 'Courier New', monospace;
|
||||
text-shadow: 0 0 2px rgba(0,255,65,0.4);
|
||||
}
|
||||
|
||||
.nav-brand { font-size: 17px; font-weight: 700; color: var(--accent); text-decoration: none; }
|
||||
/* ── Layout: sidebar + main ─────────────────────────────────────── */
|
||||
.app { display: flex; min-height: 100vh; }
|
||||
|
||||
.nav-links a { color: var(--muted); text-decoration: none; font-size: 14px; margin-left: 16px; }
|
||||
.nav-links a:hover { color: var(--accent); }
|
||||
.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; }
|
||||
|
||||
.container { max-width: 960px; margin: 0 auto; padding: 32px 20px; }
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
.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); }
|
||||
.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; }
|
||||
@@ -64,25 +175,15 @@
|
||||
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: #fff; }
|
||||
.btn-primary { background: var(--accent); color: var(--btn-light-text); }
|
||||
.btn-danger { background: var(--red); color: #fff; }
|
||||
.btn-success { background: var(--green); color: #fff; }
|
||||
.btn-success { background: var(--green); color: var(--btn-light-text); }
|
||||
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.btn-primary { color: #000; }
|
||||
.btn-success { color: #000; }
|
||||
}
|
||||
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px;
|
||||
font-size: 12px; font-weight: 600; }
|
||||
.badge-green { background: #d1fae5; color: #065f46; }
|
||||
.badge-red { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.badge-green { background: #064e3b; color: #6ee7b7; }
|
||||
.badge-red { background: #7f1d1d; color: #fca5a5; }
|
||||
}
|
||||
.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;
|
||||
@@ -108,6 +209,9 @@
|
||||
|
||||
.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; }
|
||||
@@ -132,15 +236,55 @@
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a class="nav-brand" href="/">SimpleX Manager</a>
|
||||
<div class="nav-links">
|
||||
<a href="/">Profiles</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
<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="/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">
|
||||
<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 = '›';
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user