- main.py: FastAPI app with profile CRUD, start/stop, send message endpoints - profiles.py: asyncio bot lifecycle using simplex-chat Python SDK - db.py: SQLite registry tracking profiles, types, config, addresses - templates/: Jinja2 + HTMX web UI - login.html: token-based auth - index.html: profile list with live status polling, create dialog - profile.html: per-bot dashboard with QR code, contacts/groups, event log, send form - requirements.txt: fastapi, uvicorn, jinja2, simplex-chat - start.sh: one-command startup with venv bootstrap Bot types: echo, broadcast, support (business address), directory, deadmans Run: cd manager && MANAGER_TOKEN=secret ./start.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
4.1 KiB
HTML
113 lines
4.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Profiles — SimpleX Manager{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="flex-between" style="margin-bottom: 24px;">
|
|
<h1 style="margin:0">Bot Profiles</h1>
|
|
<button class="btn btn-primary" onclick="document.getElementById('create-dialog').showModal()">+ New Profile</button>
|
|
</div>
|
|
|
|
{% if profiles %}
|
|
<div id="profile-list">
|
|
{% for p in profiles %}
|
|
<div class="card" id="profile-{{ p.id }}">
|
|
<div class="flex-between">
|
|
<div class="flex gap-8">
|
|
<strong>{{ p.name }}</strong>
|
|
<span class="tag">{{ 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">
|
|
<a href="/profile/{{ p.id }}" class="btn btn-ghost" style="padding: 6px 14px; font-size: 13px;">View</a>
|
|
<button class="btn btn-success" style="padding: 6px 14px; font-size: 13px;"
|
|
hx-post="/api/profiles/{{ p.id }}/start"
|
|
hx-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
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-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
hx-swap="none"
|
|
onclick="this.textContent='Stopping…'">Stop</button>
|
|
</div>
|
|
</div>
|
|
{% if p.address %}
|
|
<div class="muted mt-8 monospace" style="word-break:break-all;">{{ p.address }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="card" style="text-align:center; padding: 48px; color: var(--muted);">
|
|
No profiles yet. Create one to get started.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Create dialog -->
|
|
<dialog id="create-dialog">
|
|
<h2 style="margin-bottom:20px;">New Bot Profile</h2>
|
|
<form id="create-form">
|
|
<div class="field">
|
|
<label>Name</label>
|
|
<input type="text" name="name" placeholder="My Support Bot" required>
|
|
</div>
|
|
<div class="field">
|
|
<label>Bot Type</label>
|
|
<select name="bot_type">
|
|
{% for t in bot_types %}
|
|
<option value="{{ t }}">{{ t }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Welcome Message</label>
|
|
<input type="text" name="welcome_message" placeholder="Welcome! How can I help?">
|
|
</div>
|
|
<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) {}
|
|
}
|
|
|
|
document.getElementById('create-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault()
|
|
const fd = new FormData(e.target)
|
|
const config = {}
|
|
const welcome = fd.get('welcome_message')
|
|
if (welcome) config.welcome_message = welcome
|
|
|
|
const resp = await fetch('/api/profiles', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', 'X-Token': document.cookie.match(/token=([^;]+)/)?.[1] || ''},
|
|
body: JSON.stringify({name: fd.get('name'), bot_type: fd.get('bot_type'), config})
|
|
})
|
|
if (resp.ok) {
|
|
document.getElementById('create-dialog').close()
|
|
location.reload()
|
|
} else {
|
|
const err = await resp.json()
|
|
alert('Error: ' + (err.detail || 'unknown'))
|
|
}
|
|
})
|
|
</script>
|
|
{% endblock %}
|