New RSS_TYPES category with a /rss-bots page, sidebar entry, homepage tile, and an explanation that RSS bots post a feed to a channel — share the channel link, not the user. Remove rss from the Bots page (types table + create dropdown); the RSS Bots page has a '+ New RSS Bot' button that only creates rss bots, with feed URL + per hour/day/week fields shown directly (no bot-type select). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
456 lines
23 KiB
HTML
456 lines
23 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>
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.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: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||
background: var(--bg); color: var(--text); min-height: 100vh; }
|
||
|
||
/* Material-ish elevation: cards lift slightly on hover */
|
||
.card { transition: box-shadow 0.18s ease, transform 0.05s ease; }
|
||
.card:hover { box-shadow: 0 4px 18px rgba(0,0,0,0.14); }
|
||
[data-theme="matrix"] .card:hover { box-shadow: 0 0 18px rgba(0,255,65,0.22); }
|
||
.btn { box-shadow: 0 1px 3px rgba(0,0,0,0.18); letter-spacing: 0.2px; }
|
||
.btn-ghost { box-shadow: none; }
|
||
|
||
[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; }
|
||
|
||
.side-head { display: flex; align-items: center; border-bottom: 1px solid var(--border); }
|
||
.collapse-toggle {
|
||
flex-shrink: 0; background: none; border: none; cursor: pointer;
|
||
color: var(--muted); font-size: 17px; padding: 18px;
|
||
}
|
||
.collapse-toggle:hover { color: var(--text); }
|
||
.nav-brand {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 18px 18px 18px 0; font-size: 16px; font-weight: 700;
|
||
color: var(--accent); text-decoration: none;
|
||
white-space: nowrap; overflow: hidden;
|
||
}
|
||
.nav-brand .brand-icon { font-size: 18px; flex-shrink: 0; }
|
||
html.collapsed .nav-brand { display: none; }
|
||
html.collapsed .side-head { justify-content: center; }
|
||
|
||
.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 .side-nav .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-clock { padding: 12px 18px 10px; border-bottom: 1px solid var(--border); text-align: center; }
|
||
.side-clock .clk-time { font-size: 20px; font-weight: 700; color: var(--text);
|
||
font-variant-numeric: tabular-nums; letter-spacing: 0.5px; }
|
||
.side-clock .clk-date { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
||
html.collapsed .side-clock { padding: 10px 0; }
|
||
html.collapsed .side-clock .clk-time { font-size: 12px; }
|
||
html.collapsed .side-clock .clk-date { display: none; }
|
||
|
||
.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); }
|
||
/* collapsed: keep only the status dot, centered */
|
||
html.collapsed .side-status { padding: 12px 0; }
|
||
html.collapsed .side-status .ss-text { display: none; }
|
||
html.collapsed .side-status .ss-row { justify-content: center; margin-top: 0; }
|
||
html.collapsed .side-status .ss-row.ss-text { display: none; }
|
||
|
||
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; }
|
||
html.collapsed .nav-brand { display: flex; } /* keep brand visible on mobile */
|
||
html.collapsed .side-head { justify-content: flex-start; }
|
||
body.sidebar-open .sidebar { transform: translateX(0); }
|
||
body.sidebar-open .backdrop { display: block; }
|
||
.mobile-menu-btn { display: flex; }
|
||
.collapse-toggle { display: none; } /* mobile uses the drawer toggle instead */
|
||
.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); }
|
||
|
||
/* reusable SimpleX link box: Link + QR toggles, both hidden by default */
|
||
.lb-btn { display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 4px 10px; font-size: 12px; font-weight: 600; border-radius: 6px;
|
||
background: transparent; border: 1px solid var(--border); color: var(--accent);
|
||
cursor: pointer; font-family: inherit; }
|
||
.lb-btn:hover { background: var(--bg); }
|
||
.lb-btn.on { background: var(--accent); color: var(--btn-light-text); border-color: var(--accent); }
|
||
.lb-link { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
||
.lb-link .addr-link { flex: 1; min-width: 0; color: var(--muted); font-family: monospace;
|
||
font-size: 12px; text-decoration: none; word-break: break-all; }
|
||
.lb-link .addr-link:hover { color: var(--accent); text-decoration: underline; }
|
||
.lb-qr { margin-top: 10px; }
|
||
.lb-qr canvas { background: #fff; border-radius: 8px; padding: 8px; }
|
||
</style>
|
||
{% block head %}{% endblock %}
|
||
</head>
|
||
<body>
|
||
<button class="mobile-menu-btn" onclick="toggleSidebar()" aria-label="Menu"><i class="fa-solid fa-bars"></i></button>
|
||
<div class="app">
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="side-head">
|
||
<button class="collapse-toggle" onclick="toggleCollapse()" title="Collapse sidebar" aria-label="Collapse sidebar"><i class="fa-solid fa-bars"></i></button>
|
||
<a class="nav-brand" href="/">
|
||
<span class="brand-text">SimpleX Manager</span>
|
||
</a>
|
||
</div>
|
||
<nav class="side-nav">
|
||
<!-- Group 1: accounts -->
|
||
<a href="/users" {% if nav_active == 'users' %}class="active"{% endif %}><span class="ico"><i class="fa-solid fa-user"></i></span><span class="lbl">Users</span></a>
|
||
<a href="/bots" {% if nav_active == 'bots' %}class="active"{% endif %}><span class="ico"><i class="fa-solid fa-robot"></i></span><span class="lbl">Bots</span></a>
|
||
<a href="/rss-bots" {% if nav_active == 'rss-bots' %}class="active"{% endif %}><span class="ico"><i class="fa-solid fa-rss"></i></span><span class="lbl">RSS Bots</span></a>
|
||
<a href="/businesses" {% if nav_active == 'businesses' %}class="active"{% endif %}><span class="ico"><i class="fa-solid fa-briefcase"></i></span><span class="lbl">Business Groups</span></a>
|
||
<a href="https://simplex.chat/file/" target="_blank" rel="noopener"><span class="ico"><i class="fa-solid fa-upload"></i></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"><i class="fa-solid fa-comments"></i></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"><i class="fa-solid fa-file-export"></i></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"><i class="fa-solid fa-envelope"></i></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"><i class="fa-solid fa-tower-broadcast"></i></span><span class="lbl">Network</span></a>
|
||
<a href="/notifications" {% if nav_active == 'notifications' %}class="active"{% endif %}><span class="ico"><i class="fa-solid fa-bell"></i></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"><i class="fa-solid fa-gear"></i></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"><i class="fa-solid fa-download"></i></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 & 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 class="ss-text"><span id="ss-running">–/–</span> running</span></div>
|
||
<div class="ss-row ss-text"><i class="fa-solid fa-server" style="width:14px;text-align:center;"></i> <span id="ss-servers">–</span></div>
|
||
<div class="ss-row ss-text" id="ss-ops" style="opacity:0.8;"></div>
|
||
</a>
|
||
<div class="side-clock" id="side-clock">
|
||
<div class="clk-time" id="clk-time">--:--</div>
|
||
<div class="clk-date ss-text" id="clk-date">—</div>
|
||
</div>
|
||
<nav class="side-nav">
|
||
<a href="/logout"><span class="ico"><i class="fa-solid fa-right-from-bracket"></i></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' : '');
|
||
}
|
||
|
||
// Clipboard that also works over plain-HTTP LAN (navigator.clipboard needs a secure context).
|
||
function robustCopy(text) {
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
return navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
|
||
}
|
||
return Promise.resolve(fallbackCopy(text));
|
||
}
|
||
function fallbackCopy(text) {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||
try { document.execCommand('copy'); } catch (e) {}
|
||
document.body.removeChild(ta);
|
||
}
|
||
function flashCheck(btn) {
|
||
const o = btn.innerHTML;
|
||
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
||
setTimeout(() => btn.innerHTML = o, 1500);
|
||
}
|
||
|
||
// Reusable SimpleX link box: Link + QR toggles (both hidden by default).
|
||
function _sxUrl(id) { const a = document.getElementById('lb-url-' + id); return a ? a.getAttribute('href') : ''; }
|
||
function sxToggleLink(id, btn) {
|
||
const el = document.getElementById('lb-link-' + id); if (!el) return;
|
||
const show = el.style.display === 'none';
|
||
el.style.display = show ? '' : 'none';
|
||
btn.classList.toggle('on', show);
|
||
}
|
||
function sxToggleQr(id, btn) {
|
||
const w = document.getElementById('lb-qr-' + id); if (!w) return;
|
||
const show = w.style.display === 'none';
|
||
w.style.display = show ? '' : 'none';
|
||
btn.classList.toggle('on', show);
|
||
if (show && !w.dataset.r && window.QRCode) {
|
||
QRCode.toCanvas(document.getElementById('lb-qrc-' + id), _sxUrl(id), {width: 180}, () => {});
|
||
w.dataset.r = '1';
|
||
}
|
||
}
|
||
function sxCopy(id, btn) { robustCopy(_sxUrl(id)).then(() => flashCheck(btn)); }
|
||
|
||
// Sidebar clock: 24h time + day-of-week date
|
||
function tickClock() {
|
||
const now = new Date();
|
||
const te = document.getElementById('clk-time');
|
||
const de = document.getElementById('clk-date');
|
||
if (te) te.textContent = now.toLocaleTimeString('en-GB', {hour: '2-digit', minute: '2-digit', hour12: false});
|
||
if (de) de.textContent = now.toLocaleDateString('en-GB', {weekday: 'short', day: '2-digit', month: 'short', year: 'numeric'});
|
||
}
|
||
tickClock();
|
||
setInterval(tickClock, 1000);
|
||
|
||
// 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 label = d.unread > 99 ? '99+' : d.unread;
|
||
// update every badge (sidebar nav + homepage card) from the one source
|
||
document.querySelectorAll('.notif-badge').forEach(b => {
|
||
if (d.unread > 0) { b.textContent = label; 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>
|