@@ -485,10 +485,40 @@
485 485 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<button class="sm primary" onclick="quickJoin()">join</button>
486 486 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</div>
487 487 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</div>
488 488 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<div id="channels-list"><div class="empty">no channels joined yet — type a channel name above</div></div>
489 489 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</div>
490 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
491 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- topology panel -->
492 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-topology">
493 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-topology',event)">
494 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h2>topology</h2><span class="card-desc">channel types, provisioning rules, active task channels</span><span class="collapse-icon">▾</span>
495 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
496 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:6px;align-items:center">
497 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="provision-channel-input" placeholder="#project.name" style="width:160px;padding:5px 8px;font-size:12px" autocomplete="off">
498 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="provisionChannel()">provision</button>
499 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
500 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
501 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" style="padding:0">
502 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="topology-types"></div>
503 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="topology-active" style="padding:12px 16px"><div class="empty">loading topology…</div></div>
504 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
505 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
506 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
507 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- ROE templates -->
508 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-roe">
509 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-roe',event)">
510 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h2>ROE templates</h2><span class="card-desc">rules-of-engagement presets for agent registration</span><span class="collapse-icon">▾</span>
511 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
512 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="event.stopPropagation();savePolicies()">save</button>
513 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
514 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
515 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p style="font-size:12px;color:#8b949e;margin-bottom:12px">Define ROE templates applied to agents at registration. Includes channels, permissions, and rate limits.</p>
516 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="roe-list"></div>
517 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="addROETemplate()" style="margin-top:10px">+ add template</button>
518 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
519 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
490 520 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</div>
491 521 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</div>
492 522 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
493 523 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<!-- CHAT -->
494 524 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<div class="tab-pane" id="pane-chat">
@@ -1726,10 +1756,19 @@
1726 1756 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
allChannels = (data.channels || []).sort();
1727 1757 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
renderChanList();
1728 1758 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
} catch(e) {
1729 1759 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('channels-list').innerHTML = '<div style="padding:16px">'+renderAlert('error', e.message)+'</div>';
1730 1760 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
1761 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadTopology();
1762 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Load ROE templates from policies for the ROE card.
1763 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1764 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const s = await api('GET', '/v1/settings');
1765 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (s && s.policies) {
1766 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies = s.policies;
1767 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderROETemplates(s.policies.roe_templates || []);
1768 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1769 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {}
1731 1770 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
1732 1771 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
1733 1772 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
function renderChanList() {
1734 1773 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
const q = (document.getElementById('chan-search').value||'').toLowerCase();
1735 1774 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
const filtered = allChannels.filter(ch => !q || ch.toLowerCase().includes(q));
@@ -1764,10 +1803,138 @@
1764 1803 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
await loadChanTab();
1765 1804 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
renderChanSidebar((await api('GET','/v1/channels')).channels||[]);
1766 1805 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
} catch(e) { alert('Join failed: '+e.message); }
1767 1806 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
1768 1807 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('quick-join-input').addEventListener('keydown', e => { if(e.key==='Enter')quickJoin(); });
1808 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1809 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- topology panel (#115) + task channels (#114) ---
1810 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadTopology() {
1811 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1812 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', '/v1/topology');
1813 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderTopologyTypes(data.types || []);
1814 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderTopologyActive(data.active_channels || [], data.types || []);
1815 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1816 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('topology-types').innerHTML = '';
1817 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('topology-active').innerHTML = '<div style="color:#8b949e;font-size:12px">topology not configured</div>';
1818 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1819 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1820 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1821 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderTopologyTypes(types) {
1822 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!types.length) { document.getElementById('topology-types').innerHTML = ''; return; }
1823 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rows = types.map(t => {
1824 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const ttl = t.ttl_seconds > 0 ? `${Math.round(t.ttl_seconds/3600)}h` : '—';
1825 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const tags = [];
1826 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (t.ephemeral) tags.push('<span style="background:#f8514922;color:#f85149;padding:1px 5px;border-radius:3px;font-size:10px">ephemeral</span>');
1827 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (t.supervision) tags.push(`<span style="font-size:11px;color:#8b949e">→ ${esc(t.supervision)}</span>`);
1828 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<tr>
1829 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><strong>${esc(t.name)}</strong></td>
1830 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><code style="font-size:11px">#${esc(t.prefix)}*</code></td>
1831 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="font-size:12px">${(t.autojoin||[]).map(n => `<code style="font-size:11px;background:#21262d;padding:1px 4px;border-radius:3px">${esc(n)}</code>`).join(' ')}</td>
1832 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="font-size:12px">${ttl}</td>
1833 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td>${tags.join(' ')}</td>
1834 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </tr>`;
1835 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }).join('');
1836 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('topology-types').innerHTML = `<table><thead><tr><th>type</th><th>prefix</th><th>autojoin</th><th>TTL</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
1837 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1838 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1839 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderTopologyActive(channels, types) {
1840 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('topology-active');
1841 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const tasks = channels.filter(c => c.ephemeral || c.type === 'task');
1842 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!tasks.length) {
1843 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = '<div style="color:#8b949e;font-size:12px;padding:4px 0">no active task channels</div>';
1844 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
1845 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1846 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rows = tasks.map(c => {
1847 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const age = c.provisioned_at ? timeSince(new Date(c.provisioned_at)) : '—';
1848 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const ttl = c.ttl_seconds > 0 ? `${Math.round(c.ttl_seconds/3600)}h` : '—';
1849 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<tr>
1850 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><strong>${esc(c.name)}</strong></td>
1851 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="font-size:12px;color:#8b949e">${esc(c.type || '—')}</td>
1852 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="font-size:12px">${age}</td>
1853 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="font-size:12px">${ttl}</td>
1854 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><button class="sm danger" onclick="dropChannel('${esc(c.name)}')">drop</button></td>
1855 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </tr>`;
1856 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }).join('');
1857 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<table><thead><tr><th>channel</th><th>type</th><th>age</th><th>TTL</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
1858 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1859 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1860 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function timeSince(date) {
1861 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const s = Math.floor((new Date() - date) / 1000);
1862 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (s < 60) return s + 's';
1863 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (s < 3600) return Math.floor(s/60) + 'm';
1864 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (s < 86400) return Math.floor(s/3600) + 'h';
1865 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return Math.floor(s/86400) + 'd';
1866 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1867 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1868 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function provisionChannel() {
1869 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let ch = document.getElementById('provision-channel-input').value.trim();
1870 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch) return;
1871 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch.startsWith('#')) ch = '#' + ch;
1872 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1873 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', '/v1/channels', {name: ch});
1874 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('provision-channel-input').value = '';
1875 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadTopology();
1876 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadChanTab();
1877 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Provision failed: ' + e.message); }
1878 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1879 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1880 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function dropChannel(ch) {
1881 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!confirm('Drop channel ' + ch + '? This unregisters it from ChanServ.')) return;
1882 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = ch.replace(/^#/,'');
1883 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1884 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('DELETE', `/v1/topology/channels/${slug}`);
1885 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadTopology();
1886 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadChanTab();
1887 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Drop failed: ' + e.message); }
1888 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1889 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1890 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- ROE template editor (#118) ---
1891 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderROETemplates(templates) {
1892 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('roe-list');
1893 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!templates || !templates.length) {
1894 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = '<div style="color:#8b949e;font-size:12px">No ROE templates defined. Click + add template to create one.</div>';
1895 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
1896 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1897 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = templates.map((t, i) => `
1898 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="border:1px solid #21262d;border-radius:6px;padding:12px;margin-bottom:10px;background:#0d1117">
1899 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:10px;align-items:center;margin-bottom:8px">
1900 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" value="${esc(t.name)}" placeholder="template name" style="flex:1;font-weight:600" onchange="updateROE(${i},'name',this.value)">
1901 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="removeROE(${i})">remove</button>
1902 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1903 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:6px">
1904 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;min-width:200px"><label style="font-size:11px">channels (comma-separated)</label><input type="text" value="${esc((t.channels||[]).join(', '))}" onchange="updateROE(${i},'channels',this.value)" style="width:100%"></div>
1905 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;min-width:200px"><label style="font-size:11px">permissions</label><input type="text" value="${esc((t.permissions||[]).join(', '))}" onchange="updateROE(${i},'permissions',this.value)" style="width:100%"></div>
1906 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1907 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:10px">
1908 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div><label style="font-size:11px">msg/sec</label><input type="number" value="${t.rate_limit?.messages_per_second||''}" placeholder="10" style="width:70px" onchange="updateROERateLimit(${i},'messages_per_second',this.value)"></div>
1909 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div><label style="font-size:11px">burst</label><input type="number" value="${t.rate_limit?.burst||''}" placeholder="50" style="width:70px" onchange="updateROERateLimit(${i},'burst',this.value)"></div>
1910 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1"><label style="font-size:11px">description</label><input type="text" value="${esc(t.description||'')}" onchange="updateROE(${i},'description',this.value)" style="width:100%"></div>
1911 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1912 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1913 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `).join('');
1914 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1915 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1916 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function addROETemplate() {
1917 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!currentPolicies.roe_templates) currentPolicies.roe_templates = [];
1918 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies.roe_templates.push({name: 'new-template', channels: [], permissions: []});
1919 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderROETemplates(currentPolicies.roe_templates);
1920 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1921 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function removeROE(i) {
1922 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies.roe_templates.splice(i, 1);
1923 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderROETemplates(currentPolicies.roe_templates);
1924 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1925 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function updateROE(i, field, val) {
1926 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (field === 'channels' || field === 'permissions') {
1927 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies.roe_templates[i][field] = val.split(',').map(s => s.trim()).filter(Boolean);
1928 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1929 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies.roe_templates[i][field] = val;
1930 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1931 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1932 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function updateROERateLimit(i, field, val) {
1933 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!currentPolicies.roe_templates[i].rate_limit) currentPolicies.roe_templates[i].rate_limit = {};
1934 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies.roe_templates[i].rate_limit[field] = Number(val) || 0;
1935 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1769 1936 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
1770 1937 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
// --- chat ---
1771 1938 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
let chatChannel = null, chatSSE = null;
1772 1939 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
1773 1940 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
async function loadChannels() {
1774 1941 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!