Files
simplex-manager/manager/templates/profile.html
Jon 270766b99b Add tiled homepage; rename Business→Businesses; link footer copyright
- Home page (home.html) at / shows the sidebar sections as tiles; reachable by
  clicking the 'SimpleX Manager' brand in the sidebar (was redirecting to /users).
- Rename the category to 'Businesses' (route /businesses, tab/nav/_category),
  keeping the per-account bot_type 'business'. Fix profile back-link label.
- Footer: link 'Bournemouth Technology Ltd' -> bournemouthtechnology.co.uk and
  'SimpleX Network' -> simplex.chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:45:28 +01:00

617 lines
26 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ profile.name }} — SimpleX Manager{% endblock %}
{% block head %}
<style>
.qr-wrap { text-align: center; padding: 16px; }
.qr-wrap canvas { border-radius: 8px; }
.msg-btn {
padding: 3px 10px; font-size: 12px; border-radius: 6px;
background: transparent; border: 1px solid var(--border);
color: var(--accent); cursor: pointer; font-weight: 600;
font-family: inherit; white-space: nowrap;
transition: background 0.15s, color 0.15s;
}
.msg-btn:hover { background: var(--accent); color: var(--btn-light-text); }
.msg-btn-danger { color: var(--red); border-color: var(--red); }
.msg-btn-danger:hover { background: var(--red); color: #fff; }
.addr-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.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; }
</style>
{% endblock %}
{% block content %}
<div class="flex-between" style="margin-bottom: 20px;">
<div class="flex gap-8">
<a href="{{ back }}" class="muted" style="text-decoration:none;">← {{ 'Users' if back == '/users' else ('Businesses' if back == '/businesses' else 'Bots') }}</a>
<span class="muted">/</span>
<strong>{{ profile.name }}</strong>
<span class="tag {% if profile.bot_type == 'user' %}tag-user{% endif %}">{{ profile.bot_type }}</span>
<span class="badge {% if profile.running %}badge-green{% else %}badge-red{% endif %}" id="status-badge">
{% if profile.running %}running{% else %}stopped{% endif %}
</span>
</div>
<div class="flex gap-8">
{% if profile.running %}
<button class="btn btn-danger"
hx-post="/api/profiles/{{ profile.id }}/stop"
hx-swap="none"
hx-on::after-request="location.reload()">Stop</button>
{% else %}
<button class="btn btn-success"
hx-post="/api/profiles/{{ profile.id }}/start"
hx-swap="none"
hx-on::after-request="location.reload()">Start</button>
{% endif %}
<button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
</div>
</div>
<div class="grid-2">
<!-- Left column -->
<div>
<!-- Profile -->
<div class="card">
<div class="flex-between" style="margin-bottom:14px;">
<h2 style="margin:0;">Profile</h2>
<button class="btn btn-ghost" style="padding:6px 14px;font-size:13px;" onclick="openEdit()">Edit</button>
</div>
<div class="flex gap-8" style="align-items:flex-start;">
{% if profile.config.avatar %}
<img src="{{ profile.config.avatar }}" alt="avatar"
style="width:64px;height:64px;border-radius:50%;object-fit:cover;border:1px solid var(--border);flex-shrink:0;">
{% else %}
<div style="width:64px;height:64px;border-radius:50%;background:var(--border);flex-shrink:0;
display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:700;color:var(--muted);">
{{ profile.name[0] | upper }}
</div>
{% endif %}
<div style="min-width:0;">
<div style="font-weight:700;font-size:16px;">{{ profile.name }}</div>
{% if profile.config.full_name %}<div class="muted">{{ profile.config.full_name }}</div>{% endif %}
{% if profile.config.bio %}
<div style="margin-top:6px;font-size:14px;white-space:pre-wrap;">{{ profile.config.bio }}</div>
{% else %}
<div class="muted" style="margin-top:6px;">No bio set.</div>
{% endif %}
</div>
</div>
</div>
<!-- Address / QR -->
<div class="card">
<h2>Address</h2>
{% if profile.address %}
<div class="addr-row">
<button class="btn btn-ghost copy-btn" title="Copy address"
onclick="copyAddr(this, '{{ profile.address | e }}')">📋</button>
<a class="addr-link" href="{{ profile.address }}" target="_blank" rel="noopener" id="address-text">{{ profile.address }}</a>
</div>
<div class="qr-wrap">
<canvas id="qr-canvas"></canvas>
<p class="muted" style="margin-top:10px;">Scan QR code from mobile app to start a chat</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
<script>
QRCode.toCanvas(document.getElementById('qr-canvas'), {{ profile.address | tojson }}, {width: 200}, () => {})
</script>
{% else %}
<p class="muted">Start the profile to generate an address.</p>
{% endif %}
</div>
{% if profile.bot_type == 'directory' %}
{% set safe = profile.name | lower | replace(' ', '_') %}
<!-- Directory website -->
<div class="card">
<h2>Directory website</h2>
<p class="muted" style="margin-bottom:12px;">Auto-generated listing page for this directory bot.</p>
<div class="addr-row">
<button class="btn btn-ghost copy-btn" title="Copy URL"
onclick="copyAddr(this, location.origin + '/directory/{{ safe }}/index.html')">📋</button>
<a class="addr-link" href="/directory/{{ safe }}/index.html" target="_blank" rel="noopener">/directory/{{ safe }}/index.html</a>
</div>
</div>
{% endif %}
<!-- Config -->
<div class="card">
<h2>Config</h2>
<table>
<tr><th>Key</th><th>Value</th></tr>
{% for k, v in profile.config.items() if k not in ['avatar', 'bio', 'full_name'] %}
<tr><td>{{ k }}</td><td>{% if k == 'api_key' %}•••••••• (set){% else %}{{ v }}{% endif %}</td></tr>
{% else %}
<tr><td colspan="2" class="muted">No extra config set.</td></tr>
{% endfor %}
</table>
</div>
</div>
<!-- Right column -->
<div>
<!-- Contacts -->
<div class="card">
<h2>Contacts ({{ contacts | length }})</h2>
{% if contacts %}
<table>
<tr><th>Name</th><th style="width:50px;"></th></tr>
{% for c in contacts %}
<tr>
<td><strong>{{ c.localDisplayName }}</strong></td>
<td>
<div class="flex gap-8" style="justify-content:flex-end;">
<a class="msg-btn" style="text-decoration:none;"
href="/profile/{{ profile.id }}/chat/direct/{{ c.contactId }}">💬 Chat</a>
<button class="msg-btn" title="Clear conversation"
onclick="clearChat('direct', {{ c.contactId }}, '{{ c.localDisplayName | e }}')">🧹 Clear</button>
<button class="msg-btn msg-btn-danger" title="Delete contact"
onclick="deleteContact({{ c.contactId }}, '{{ c.localDisplayName | e }}')">🗑 Delete</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="muted">No contacts yet.</p>
{% endif %}
</div>
{# Macro: one group/channel row. api_list_groups gives bare GroupInfo dicts:
g.groupId, g.groupProfile.displayName, g.groupSummary.currentMembers.
The verb is "Post" for channels (broadcast) and "Msg" for groups. #}
{% macro groupRow(g) %}
{% set name = g.groupProfile.displayName %}
{% set gid = g.groupId %}
{% set mcnt = g.groupSummary.currentMembers %}
{% set invited = (g.membership.memberStatus if g.membership else '') == 'invited' %}
{% set is_owner = (g.membership.memberRole if g.membership else '') == 'owner' %}
<tr>
<td>{{ name }}</td>
<td>
{% if invited %}
<span class="tag" title="You were invited but haven't joined yet">⏳ invited</span>
{% else %}
<button class="msg-btn" style="border:none;padding:0;background:none;color:var(--accent);font-weight:600;font-size:13px;cursor:pointer;"
onclick="loadMembers({{ gid }}, '{{ name | e }}')">{{ mcnt }}</button>
{% endif %}
</td>
<td>
<div class="flex gap-8">
{% if invited %}
<button class="msg-btn" onclick="joinGroup({{ gid }}, this)">Join</button>
{% else %}
<a class="msg-btn" style="text-decoration:none;"
href="/profile/{{ profile.id }}/chat/group/{{ gid }}">💬 {{ 'Broadcast' if g.is_channel else 'Chat' }}</a>
<button class="msg-btn" onclick="getGroupLink({{ gid }}, this)">Link</button>
<button class="msg-btn msg-btn-danger" onclick="leaveGroup({{ gid }}, '{{ name | e }}', this)">Leave</button>
{% if is_owner %}
<button class="msg-btn msg-btn-danger" onclick="deleteGroup({{ gid }}, '{{ name | e }}', this)">Delete</button>
{% endif %}
{% endif %}
</div>
</td>
</tr>
{% endmacro %}
<!-- Groups -->
<div class="card">
<div class="flex-between" style="margin-bottom:12px;">
<h2 style="margin:0;">Groups ({{ groups | length }})</h2>
{% if profile.running %}
<button class="btn btn-primary" style="padding:6px 14px;font-size:13px;"
onclick="openCreate('group')">+ Create Group</button>
{% endif %}
</div>
{% if groups %}
<table>
<tr><th>Name</th><th>Members</th><th style="width:130px;"></th></tr>
{% for g in groups %}{{ groupRow(g) }}{% endfor %}
</table>
{% else %}
<p class="muted">No groups yet.{% if not profile.running %} Start the profile first.{% endif %}</p>
{% endif %}
</div>
<!-- Channels -->
<div class="card">
<div class="flex-between" style="margin-bottom:12px;">
<h2 style="margin:0;">Channels ({{ channels | length }})</h2>
{% if profile.running %}
<button class="btn btn-primary" style="padding:6px 14px;font-size:13px;"
onclick="openCreate('channel')">+ Create Channel</button>
{% endif %}
</div>
{% if channels %}
<table>
<tr><th>Name</th><th>Subscribers</th><th style="width:130px;"></th></tr>
{% for g in channels %}{{ groupRow(g) }}{% endfor %}
</table>
{% else %}
<p class="muted">No channels yet.{% if not profile.running %} Start the profile first.{% endif %}</p>
{% endif %}
</div>
<!-- Event log -->
<div class="card">
<div class="flex-between" style="margin-bottom:10px;">
<h2 style="margin:0;">Event Log</h2>
<button class="btn btn-ghost" style="font-size:12px;padding:4px 10px;"
hx-get="/api/profiles/{{ profile.id }}/status"
hx-swap="none"
hx-on::after-request="refreshLog(event)">Refresh</button>
</div>
<div class="log-box" id="log-box">{% for line in log_lines %}{{ line }}
{% endfor %}</div>
</div>
</div>
</div>
<!-- Create group/channel dialog -->
<dialog id="ch-dialog">
<h2 style="margin-bottom:16px;" id="ch-title">Create Group</h2>
<p class="muted" style="margin-bottom:16px;font-size:13px;" id="ch-desc"></p>
<div class="field">
<label id="ch-name-label">Group Name</label>
<input type="text" id="ch-name" placeholder="My Group" required>
</div>
<div id="ch-link-wrap" style="display:none;margin-bottom:12px;">
<label>Join Link</label>
<div class="flex gap-8">
<input type="text" id="ch-link-out" readonly style="font-family:monospace;font-size:12px;">
<button class="btn btn-ghost" style="white-space:nowrap;" onclick="copyChLink()">Copy</button>
</div>
</div>
<div class="flex-between mt-16">
<span id="ch-result" class="muted" style="font-size:13px;"></span>
<div class="flex gap-8">
<button class="btn btn-ghost" onclick="closeChDialog()">Close</button>
<button class="btn btn-primary" id="ch-create-btn" onclick="createGroup()">Create</button>
</div>
</div>
</dialog>
<!-- Members dialog -->
<dialog id="members-dialog">
<div class="flex-between" style="margin-bottom:16px;">
<h2 style="margin:0;">Members — <span id="members-channel-name" style="color:var(--accent);"></span></h2>
<button class="btn btn-ghost" style="padding:4px 10px;font-size:13px;"
onclick="document.getElementById('members-dialog').close()"></button>
</div>
<div id="members-list" style="max-height:320px;overflow-y:auto;">
<p class="muted">Loading…</p>
</div>
</dialog>
<!-- Edit profile dialog -->
<dialog id="edit-dialog">
<h2 style="margin-bottom:16px;">Edit Profile</h2>
<div class="field">
<label>Full Name <span class="muted" style="font-weight:400;">(optional)</span></label>
<input type="text" id="edit-fullname">
</div>
<div class="field">
<label>Bio / Description <span class="muted" style="font-weight:400;">(optional)</span></label>
<textarea id="edit-bio" rows="2"></textarea>
</div>
<div class="field">
<label>Avatar</label>
<div class="flex gap-8">
<img id="edit-avatar-preview" alt="" style="display:none;width:48px;height:48px;border-radius:50%;object-fit:cover;border:1px solid var(--border);">
<input type="file" accept="image/*" onchange="onEditAvatar(this)" style="flex:1;">
</div>
<button type="button" class="btn btn-ghost" style="margin-top:6px;font-size:12px;padding:4px 10px;"
onclick="removeEditAvatar()">Remove avatar</button>
</div>
<div class="flex-between mt-16">
<span id="edit-result" class="muted" style="font-size:13px;"></span>
<div class="flex gap-8">
<button class="btn btn-ghost" onclick="document.getElementById('edit-dialog').close()">Cancel</button>
<button class="btn btn-primary" onclick="saveProfile()">Save</button>
</div>
</div>
</dialog>
<!-- Send message dialog -->
<dialog id="msg-dialog">
<h2 style="margin-bottom:16px;">Message <span id="msg-target-label" style="color:var(--accent);"></span></h2>
<div class="field">
<textarea id="msg-text" rows="4" placeholder="Type your message…"
style="resize:vertical;"
onkeydown="if(event.key==='Enter'&&(event.ctrlKey||event.metaKey)){sendMsg();return false;}"></textarea>
</div>
<div class="flex-between mt-16">
<span id="msg-result" class="muted" style="font-size:13px;"></span>
<div class="flex gap-8">
<button class="btn btn-ghost" onclick="document.getElementById('msg-dialog').close()">Cancel</button>
<button class="btn btn-primary" onclick="sendMsg()">Send</button>
</div>
</div>
</dialog>
<script>
let msgTarget = '';
function openMsg(name) {
msgTarget = name;
document.getElementById('msg-target-label').textContent = name;
document.getElementById('msg-text').value = '';
document.getElementById('msg-result').textContent = '';
const dlg = document.getElementById('msg-dialog');
dlg.showModal();
setTimeout(() => document.getElementById('msg-text').focus(), 50);
}
async function sendMsg() {
const text = document.getElementById('msg-text').value.trim();
if (!text) return;
const result = document.getElementById('msg-result');
result.textContent = 'Sending…';
const token = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
const resp = await fetch('/api/profiles/{{ profile.id }}/send', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Token': token},
body: JSON.stringify({to: msgTarget, text})
});
const data = await resp.json();
if (data.ok) {
document.getElementById('msg-text').value = '';
result.textContent = '✓ Sent';
setTimeout(() => document.getElementById('msg-dialog').close(), 800);
} else {
result.textContent = '✗ Failed';
}
}
function copyAddr(btn, addr) {
navigator.clipboard.writeText(addr).then(() => {
btn.textContent = '✓';
setTimeout(() => btn.textContent = '📋', 1500);
});
}
// ── Contact actions ────────────────────────────────────────────────────────
function _ctoken() { return document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || ''; }
async function clearChat(type, id, name) {
if (!confirm('Clear the conversation with ' + name + '? Messages are removed; the contact stays.')) return;
const r = await fetch(`/api/profiles/{{ profile.id }}/chat/${type}/${id}/clear`, {
method: 'POST', headers: { 'X-Token': _ctoken() },
});
const d = await r.json();
if (d.ok) { alert('Conversation cleared.'); }
else { alert('Failed: ' + (d.detail || 'unknown')); }
}
async function deleteContact(id, name) {
if (!confirm('Delete contact ' + name + '? This removes them and your conversation.')) return;
const r = await fetch(`/api/profiles/{{ profile.id }}/contacts/${id}`, {
method: 'DELETE', headers: { 'X-Token': _ctoken() },
});
const d = await r.json();
if (d.ok) { location.reload(); }
else { alert('Failed: ' + (d.detail || 'unknown')); }
}
// ── Edit profile ───────────────────────────────────────────────────────────
// editAvatar: null = unchanged, '' = remove, dataURI = replace
let editAvatar = null;
function openEdit() {
editAvatar = null;
document.getElementById('edit-fullname').value = {{ (profile.config.full_name or '') | tojson }};
document.getElementById('edit-bio').value = {{ (profile.config.bio or '') | tojson }};
const prev = document.getElementById('edit-avatar-preview');
const cur = {{ (profile.config.avatar or '') | tojson }};
if (cur) { prev.src = cur; prev.style.display = 'block'; } else { prev.style.display = 'none'; prev.src = ''; }
document.getElementById('edit-result').textContent = '';
document.getElementById('edit-dialog').showModal();
}
function onEditAvatar(input) {
const file = input.files && input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const size = 256, c = document.createElement('canvas');
c.width = size; c.height = size;
const ctx = c.getContext('2d');
const m = Math.min(img.width, img.height);
ctx.drawImage(img, (img.width - m) / 2, (img.height - m) / 2, m, m, 0, 0, size, size);
editAvatar = c.toDataURL('image/jpeg', 0.85);
const prev = document.getElementById('edit-avatar-preview');
prev.src = editAvatar; prev.style.display = 'block';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function removeEditAvatar() {
editAvatar = '';
const prev = document.getElementById('edit-avatar-preview');
prev.style.display = 'none'; prev.src = '';
}
async function saveProfile() {
const body = {
full_name: document.getElementById('edit-fullname').value,
bio: document.getElementById('edit-bio').value,
};
if (editAvatar !== null) body.avatar = editAvatar; // only send if changed
document.getElementById('edit-result').textContent = 'Saving…';
const resp = await fetch('/api/profiles/{{ profile.id }}/profile', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Token': _token()},
body: JSON.stringify(body),
});
const data = await resp.json();
if (data.ok) { location.reload(); }
else { document.getElementById('edit-result').textContent = '✗ ' + (data.detail || 'Failed'); }
}
// ── Groups & Channels ──────────────────────────────────────────────────────
const _token = () => document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
let _createKind = 'group';
function openCreate(kind) {
_createKind = kind;
const isCh = kind === 'channel';
document.getElementById('ch-title').textContent = isCh ? 'Create Channel' : 'Create Group';
document.getElementById('ch-desc').textContent = isCh
? 'Observer join link — subscribers can read broadcasts but not post. Only you broadcast.'
: 'Member join link — everyone who joins can send messages (2-way).';
document.getElementById('ch-name-label').textContent = isCh ? 'Channel Name' : 'Group Name';
document.getElementById('ch-name').placeholder = isCh ? 'My Channel' : 'My Group';
document.getElementById('ch-name').value = '';
document.getElementById('ch-link-wrap').style.display = 'none';
document.getElementById('ch-result').textContent = '';
const btn = document.getElementById('ch-create-btn');
btn.disabled = false; btn.style.display = '';
document.getElementById('ch-dialog').showModal();
}
async function createGroup() {
const name = document.getElementById('ch-name').value.trim();
if (!name) return;
const btn = document.getElementById('ch-create-btn');
btn.disabled = true;
document.getElementById('ch-result').textContent = 'Creating…';
const resp = await fetch('/api/profiles/{{ profile.id }}/groups', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Token': _token()},
body: JSON.stringify({name, kind: _createKind}),
});
const data = await resp.json();
if (data.ok) {
document.getElementById('ch-link-out').value = data.link;
document.getElementById('ch-link-wrap').style.display = '';
document.getElementById('ch-result').textContent = '✓ Created';
btn.style.display = 'none';
} else {
document.getElementById('ch-result').textContent = '✗ ' + (data.detail || 'Failed');
btn.disabled = false;
}
}
function copyChLink() {
const val = document.getElementById('ch-link-out').value;
navigator.clipboard.writeText(val).then(() => {
document.getElementById('ch-result').textContent = '✓ Copied';
});
}
function closeChDialog() {
document.getElementById('ch-dialog').close();
location.reload(); // refresh group/channel lists
}
async function loadMembers(groupId, groupName) {
document.getElementById('members-channel-name').textContent = groupName;
document.getElementById('members-list').innerHTML = '<p class="muted">Loading…</p>';
document.getElementById('members-dialog').showModal();
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}/members`, {
headers: {'X-Token': _token()},
});
const data = await resp.json();
if (!data.members || data.members.length === 0) {
document.getElementById('members-list').innerHTML =
'<p class="muted">No other members yet (you are the owner).</p>';
return;
}
const rows = data.members.map(m => `
<tr>
<td><strong>${m.name}</strong></td>
<td class="muted" style="font-size:12px;">${m.role}</td>
<td class="muted" style="font-size:12px;">${m.status}</td>
</tr>`).join('');
document.getElementById('members-list').innerHTML = `
<table>
<tr><th>Name</th><th>Role</th><th>Status</th></tr>
${rows}
</table>`;
}
async function joinGroup(groupId, btn) {
btn.textContent = 'Joining…'; btn.disabled = true;
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}/join`, {
method: 'POST', headers: { 'X-Token': _token() },
});
const data = await resp.json();
if (data.ok) { location.reload(); }
else { btn.textContent = 'Join'; btn.disabled = false; alert('Failed to join: ' + (data.detail || 'unknown')); }
}
async function deleteGroup(groupId, name, btn) {
if (!confirm('Delete "' + name + '" for everyone? This removes the group/channel and notifies members.')) return;
btn.disabled = true; btn.textContent = 'Deleting…';
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}`, {
method: 'DELETE', headers: { 'X-Token': _token() },
});
const data = await resp.json();
if (data.ok) { location.reload(); }
else { btn.disabled = false; btn.textContent = 'Delete'; alert('Failed to delete: ' + (data.detail || 'unknown')); }
}
async function leaveGroup(groupId, name, btn) {
if (!confirm('Leave "' + name + '"? You will stop receiving its messages.')) return;
btn.disabled = true; btn.textContent = 'Leaving…';
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}/leave`, {
method: 'POST', headers: { 'X-Token': _token() },
});
const data = await resp.json();
if (data.ok) { location.reload(); }
else { btn.disabled = false; btn.textContent = 'Leave'; alert('Failed to leave: ' + (data.detail || 'unknown')); }
}
async function getGroupLink(groupId, btn) {
const orig = btn.textContent;
btn.textContent = '…';
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}/link`, {
headers: {'X-Token': _token()},
});
const data = await resp.json();
if (data.link) {
await navigator.clipboard.writeText(data.link);
btn.textContent = '✓ Copied';
} else {
btn.textContent = 'No link';
}
setTimeout(() => btn.textContent = orig, 2000);
}
// ─────────────────────────────────────────────────────────────────────────────
function refreshLog(event) {
try {
const data = JSON.parse(event.detail.xhr.responseText);
document.getElementById('log-box').textContent = data.log.join('\n');
document.getElementById('status-badge').textContent = data.running ? 'running' : 'stopped';
document.getElementById('status-badge').className = 'badge ' + (data.running ? 'badge-green' : 'badge-red');
} catch(e) {}
}
function confirmDelete() {
if (!confirm('Delete this profile? This cannot be undone.')) return;
const token = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
fetch('/api/profiles/{{ profile.id }}', {
method: 'DELETE',
headers: {'X-Token': token}
}).then(() => location.href = '/');
}
{% if profile.running %}
setInterval(() => {
document.querySelector('[hx-get="/api/profiles/{{ profile.id }}/status"]')?.click();
}, 10000);
{% endif %}
</script>
{% endblock %}