Files
simplex-manager/manager/templates/list.html
Jon ecce417f6d 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>
2026-06-03 14:48:24 +01:00

184 lines
7.2 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ tab | title }} — SimpleX Manager{% endblock %}
{% block head %}
<style>
.empty-state { text-align: center; padding: 56px 24px; color: var(--muted); }
.empty-state p { margin-top: 8px; font-size: 13px; }
.profile-card { cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s, transform 0.05s;
border: 1px solid transparent; }
.profile-card:hover { border-color: var(--accent); }
.profile-card:active { transform: translateY(1px); }
.addr-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.addr-link { flex: 1; min-width: 0; color: var(--muted); font-family: monospace; font-size: 12px;
text-decoration: none; word-break: break-all; }
.addr-link:hover { color: var(--accent); text-decoration: underline; }
.copy-btn { flex-shrink: 0; padding: 4px 9px; font-size: 13px; line-height: 1; }
.bot-types-card table td { vertical-align: top; }
.bot-types-card .tag { white-space: nowrap; }
</style>
{% endblock %}
{% block content %}
<div class="flex-between" style="margin-bottom: 24px;">
<h1 style="margin:0;">{{ tab | title }}</h1>
<button class="btn btn-primary" onclick="openCreate()">
+ New {{ 'User' if tab == 'users' else 'Bot' }}
</button>
</div>
{% if tab == 'bots' %}
<div class="card bot-types-card" style="margin-bottom:24px;">
<h2 style="font-size:15px;margin-bottom:12px;">Available bot types</h2>
<table>
<tr><td><span class="tag">echo</span></td><td class="muted">Repeats every message back to the sender — handy for testing a connection end to end.</td></tr>
<tr><td><span class="tag">broadcast</span></td><td class="muted">Relays messages from authorized publishers out to all of the bot's contacts.</td></tr>
<tr><td><span class="tag">support</span></td><td class="muted">Business inbox — auto-replies with a welcome message and collects incoming inquiries.</td></tr>
<tr><td><span class="tag">directory</span></td><td class="muted">Directory service for discovering and listing groups or contacts.</td></tr>
<tr><td><span class="tag">deadmans</span></td><td class="muted">Dead man's switch — triggers an action if expected check-ins stop arriving.</td></tr>
</table>
</div>
{% endif %}
{% if items %}
{% for p in items %}
<div class="card profile-card" id="profile-{{ p.id }}"
onclick="location.href='/profile/{{ p.id }}'">
<div class="flex-between">
<div class="flex gap-8">
<strong>{{ p.name }}</strong>
<span class="tag {% if p.bot_type == 'user' %}tag-user{% endif %}">{{ p.bot_type }}</span>
<span class="badge {% if p.running %}badge-green{% else %}badge-red{% endif %}"
id="status-{{ p.id }}"
hx-get="/api/profiles/{{ p.id }}/status"
hx-trigger="every 5s"
hx-swap="none"
hx-on::after-request="updateStatus({{ p.id }}, event)">
{% if p.running %}running{% else %}stopped{% endif %}
</span>
</div>
<div class="flex gap-8" onclick="event.stopPropagation()">
<button class="btn btn-success" style="padding:6px 14px;font-size:13px;"
hx-post="/api/profiles/{{ p.id }}/start" hx-swap="none"
onclick="this.textContent='Starting…'">Start</button>
<button class="btn btn-danger" style="padding:6px 14px;font-size:13px;"
hx-post="/api/profiles/{{ p.id }}/stop" hx-swap="none"
onclick="this.textContent='Stopping…'">Stop</button>
</div>
</div>
{% if p.address %}
<div class="addr-row" onclick="event.stopPropagation()">
<button class="btn btn-ghost copy-btn" title="Copy address"
onclick="copyAddr(event, this, '{{ p.address | e }}')">📋</button>
<a class="addr-link" href="{{ p.address }}" target="_blank" rel="noopener">{{ p.address }}</a>
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="empty-state card">
{% if tab == 'users' %}
<strong>No users yet</strong>
<p>Create a SimpleX user account to manage contacts and channels.</p>
{% else %}
<strong>No bots yet</strong>
<p>Bots can echo messages, broadcast to subscribers, or run automated tasks.</p>
{% endif %}
</div>
{% endif %}
<!-- Create dialog -->
<dialog id="create-dialog">
<h2 style="margin-bottom:20px;">New {{ 'User' if tab == 'users' else 'Bot' }}</h2>
<form id="create-form">
<div class="field">
<label>Name</label>
<input type="text" name="name" placeholder="{{ 'Alice' if tab == 'users' else 'My Bot' }}" required>
</div>
{% if tab == 'bots' %}
<div class="field">
<label>Bot Type</label>
<select name="profile_type" id="type-select" onchange="onTypeChange()">
{% for t in create_types %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="field" id="welcome-field">
<label>Welcome Message</label>
<input type="text" name="welcome_message" placeholder="Welcome! How can I help?">
</div>
{% endif %}
<div class="flex gap-8 mt-16" style="justify-content:flex-end;">
<button type="button" class="btn btn-ghost"
onclick="document.getElementById('create-dialog').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</dialog>
<script>
function updateStatus(id, event) {
try {
const data = JSON.parse(event.detail.xhr.responseText);
const badge = document.getElementById('status-' + id);
if (!badge) return;
badge.textContent = data.running ? 'running' : 'stopped';
badge.className = 'badge ' + (data.running ? 'badge-green' : 'badge-red');
} catch(e) {}
}
function openCreate() {
document.getElementById('create-form').reset();
{% if tab == 'bots' %}onTypeChange();{% endif %}
document.getElementById('create-dialog').showModal();
}
function copyAddr(ev, btn, addr) {
ev.stopPropagation();
navigator.clipboard.writeText(addr).then(() => {
btn.textContent = '✓';
setTimeout(() => btn.textContent = '📋', 1500);
});
}
{% if tab == 'bots' %}
function onTypeChange() {
const val = document.getElementById('type-select').value;
const hide = ['echo'].includes(val); // echo has no welcome msg
document.getElementById('welcome-field').style.display = hide ? 'none' : '';
}
{% endif %}
document.getElementById('create-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
{% if tab == 'users' %}
const botType = 'user';
const config = {};
{% else %}
const botType = fd.get('profile_type') || '{{ create_types[0] }}';
const config = {};
const welcome = fd.get('welcome_message');
if (welcome) config.welcome_message = welcome;
{% endif %}
const token = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
const resp = await fetch('/api/profiles', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Token': token},
body: JSON.stringify({name: fd.get('name'), bot_type: botType, config}),
});
if (resp.ok) {
document.getElementById('create-dialog').close();
location.reload();
} else {
const err = await resp.json();
alert('Error: ' + (err.detail || 'unknown'));
}
});
</script>
{% endblock %}