ScuttleBot

feat(ui): add section descriptions to settings card headers Each collapsible card header now shows a short subtitle explaining what the section controls — connection, admin accounts, behaviors, agent policy, web bridge, logging, general, ergo, TLS, topology, LLM backends, supported backends, YAML example. Also untrack bin/ binaries (bin/ is already gitignored).

lmata 2026-04-03 00:11 trunk
Commit a7a32ea76bd2868efeb33e489725cb9fd4a9e6f647d9d9b83e2acd6f23657506
D bin/claude-agent

Binary file

D bin/codex-agent

Binary file

D bin/codex-relay

Binary file

D bin/fleet-cmd

Binary file

D bin/gemini-agent

Binary file

D bin/scuttlebot

Binary file

D bin/scuttlectl

Binary file

--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -29,10 +29,11 @@
2929
/* cards */
3030
.card { background:#161b22; border:1px solid #30363d; border-radius:8px; overflow:hidden; }
3131
.card-header { padding:12px 16px; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
3232
.card-header:hover { background:#1c2128; }
3333
.card-header h2 { font-size:14px; font-weight:600; }
34
+.card-header .card-desc { font-size:11px; color:#6e7681; font-weight:400; }
3435
.card-header .collapse-icon { font-size:11px; color:#8b949e; margin-left:2px; transition:transform .15s; }
3536
.card.collapsed .card-header { border-bottom:none; }
3637
.card.collapsed .card-body { display:none; }
3738
.card.collapsed .collapse-icon { transform:rotate(-90deg); }
3839
.card-body { padding:16px; }
@@ -430,11 +431,11 @@
430431
<div class="tab-pane" id="pane-settings">
431432
<div class="pane-inner">
432433
433434
<!-- connection -->
434435
<div class="card" id="card-connection">
435
- <div class="card-header" style="cursor:default"><h2>connection</h2></div>
436
+ <div class="card-header" style="cursor:default"><h2>connection</h2><span class="card-desc">current session and server endpoints</span></div>
436437
<div class="card-body">
437438
<div class="setting-row">
438439
<div class="setting-label">signed in as</div>
439440
<div class="setting-desc">Current admin session.</div>
440441
<code class="setting-val" id="settings-username-display">—</code>
@@ -458,11 +459,11 @@
458459
</div>
459460
</div>
460461
461462
<!-- admin accounts -->
462463
<div class="card" id="card-admins">
463
- <div class="card-header" onclick="toggleCard('card-admins',event)"><h2>admin accounts</h2><span class="collapse-icon">▾</span></div>
464
+ <div class="card-header" onclick="toggleCard('card-admins',event)"><h2>admin accounts</h2><span class="card-desc">who can sign in to this UI</span><span class="collapse-icon">▾</span></div>
464465
<div id="admins-list-container"></div>
465466
<div class="card-body" style="border-top:1px solid #21262d">
466467
<p style="font-size:12px;color:#8b949e;margin-bottom:12px">Add an admin account. Admins sign in at the login screen with username + password.</p>
467468
<form id="add-admin-form" onsubmit="addAdmin(event)" style="flex-direction:row;align-items:flex-end;gap:10px;flex-wrap:wrap">
468469
<div style="flex:1;min-width:130px"><label>username</label><input type="text" id="new-admin-username" autocomplete="off"></div>
@@ -473,11 +474,11 @@
473474
</div>
474475
</div>
475476
476477
<!-- tls -->
477478
<div class="card" id="card-tls">
478
- <div class="card-header" onclick="toggleCard('card-tls',event)"><h2>TLS / SSL</h2><span class="collapse-icon">▾</span><div class="spacer"></div><span id="tls-badge" class="badge">loading…</span></div>
479
+ <div class="card-header" onclick="toggleCard('card-tls',event)"><h2>TLS / SSL</h2><span class="card-desc">certificate status</span><span class="collapse-icon">▾</span><div class="spacer"></div><span id="tls-badge" class="badge">loading…</span></div>
479480
<div class="card-body">
480481
<div id="tls-status-rows"></div>
481482
<div class="alert info" style="margin-top:12px;font-size:12px">
482483
<span class="icon">ℹ</span>
483484
<span>TLS is configured in <code style="color:#a5d6ff">scuttlebot.yaml</code> under <code style="color:#a5d6ff">tls:</code>.
@@ -487,11 +488,11 @@
487488
</div>
488489
489490
<!-- system behaviors -->
490491
<div class="card" id="card-behaviors">
491492
<div class="card-header" onclick="toggleCard('card-behaviors',event)">
492
- <h2>system behaviors</h2><span class="collapse-icon">▾</span>
493
+ <h2>system behaviors</h2><span class="card-desc">bot toggles, rate limits, and default channel</span><span class="collapse-icon">▾</span>
493494
<div class="spacer"></div>
494495
<button class="sm primary" onclick="savePolicies()">save</button>
495496
</div>
496497
<div class="card-body" style="padding:0">
497498
<div id="behaviors-list"></div>
@@ -498,11 +499,11 @@
498499
</div>
499500
</div>
500501
501502
<!-- agent policy -->
502503
<div class="card" id="card-agentpolicy">
503
- <div class="card-header" onclick="toggleCard('card-agentpolicy',event)"><h2>agent policy</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
504
+ <div class="card-header" onclick="toggleCard('card-agentpolicy',event)"><h2>agent policy</h2><span class="card-desc">autojoin and check-in rules for all agents</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveAgentPolicy()">save</button></div>
504505
<div class="card-body">
505506
<div class="setting-row">
506507
<div class="setting-label">require check-in</div>
507508
<div class="setting-desc">Agents must join a coordination channel before others.</div>
508509
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
@@ -519,30 +520,58 @@
519520
<div class="setting-label">required channels</div>
520521
<div class="setting-desc">Channels every agent is added to automatically.</div>
521522
<input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
522523
</div>
523524
</div>
525
+ <div id="agentpolicy-save-result" style="display:none;margin:0 16px 12px"></div>
524526
</div>
525527
526528
<!-- bridge -->
527529
<div class="card" id="card-bridgepolicy">
528
- <div class="card-header" onclick="toggleCard('card-bridgepolicy',event)"><h2>web bridge</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
530
+ <div class="card-header" onclick="toggleCard('card-bridgepolicy',event)"><h2>web bridge</h2><span class="card-desc">IRC bot that powers the web chat UI</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveBridgeConfig()">save</button></div>
529531
<div class="card-body">
532
+ <div class="setting-row">
533
+ <div class="setting-label">enabled</div>
534
+ <div class="setting-desc">Start the bridge bot that powers the web chat UI.</div>
535
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
536
+ <input type="checkbox" id="bridge-enabled">
537
+ <span style="font-size:12px">enabled</span>
538
+ </label>
539
+ </div>
540
+ <div class="setting-row">
541
+ <div class="setting-label">nick</div>
542
+ <div class="setting-desc">IRC nick for the bridge bot. Requires restart.</div>
543
+ <input type="text" id="bridge-nick" placeholder="bridge" style="width:160px;padding:4px 8px;font-size:12px">
544
+ </div>
545
+ <div class="setting-row">
546
+ <div class="setting-label">channels</div>
547
+ <div class="setting-desc">Channels the bridge joins at startup.</div>
548
+ <input type="text" id="bridge-channels" placeholder="#general, #fleet" style="width:280px;padding:4px 8px;font-size:12px">
549
+ </div>
550
+ <div class="setting-row">
551
+ <div class="setting-label">message buffer</div>
552
+ <div class="setting-desc">Messages to keep per channel in memory.</div>
553
+ <div style="display:flex;align-items:center;gap:6px">
554
+ <input type="number" id="bridge-buffer-size" placeholder="200" min="1" style="width:80px;padding:4px 8px;font-size:12px">
555
+ <span style="font-size:12px;color:#8b949e">messages</span>
556
+ </div>
557
+ </div>
530558
<div class="setting-row">
531559
<div class="setting-label">web user TTL</div>
532560
<div class="setting-desc">How long HTTP-posted nicks stay visible in the channel user list after their last message.</div>
533561
<div style="display:flex;align-items:center;gap:6px">
534562
<input type="number" id="policy-bridge-web-user-ttl" placeholder="5" min="1" style="width:80px;padding:4px 8px;font-size:12px">
535563
<span style="font-size:12px;color:#8b949e">minutes</span>
536564
</div>
537565
</div>
538566
</div>
567
+ <div id="bridge-save-result" style="display:none;margin:0 16px 12px"></div>
539568
</div>
540569
541570
<!-- logging -->
542571
<div class="card" id="card-logging">
543
- <div class="card-header" onclick="toggleCard('card-logging',event)"><h2>message logging</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
572
+ <div class="card-header" onclick="toggleCard('card-logging',event)"><h2>message logging</h2><span class="card-desc">write channel traffic to disk</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveLogging()">save</button></div>
544573
<div class="card-body">
545574
<div class="setting-row">
546575
<div class="setting-label">enabled</div>
547576
<div class="setting-desc">Write every channel message to disk.</div>
548577
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
@@ -601,13 +630,148 @@
601630
<span style="font-size:12px;color:#8b949e">days</span>
602631
</div>
603632
</div>
604633
</div>
605634
</div>
635
+ <div id="logging-save-result" style="display:none;margin:0 16px 12px"></div>
606636
</div>
607637
608638
<div id="policies-save-result" style="display:none"></div>
639
+
640
+ <!-- general -->
641
+ <div class="card" id="card-general">
642
+ <div class="card-header" onclick="toggleCard('card-general',event)">
643
+ <h2>general</h2><span class="card-desc">API and MCP server addresses</span><span class="collapse-icon">▾</span>
644
+ <div class="spacer"></div>
645
+ <button class="sm primary" onclick="event.stopPropagation();saveGeneralConfig()">save</button>
646
+ </div>
647
+ <div class="card-body">
648
+ <div class="setting-row">
649
+ <div class="setting-label">API address</div>
650
+ <div class="setting-desc">Address scuttlebot listens on for HTTP API requests. Requires restart.</div>
651
+ <input type="text" id="general-api-addr" placeholder=":8080" style="width:160px;padding:4px 8px;font-size:12px">
652
+ </div>
653
+ <div class="setting-row">
654
+ <div class="setting-label">MCP address</div>
655
+ <div class="setting-desc">Address for the Model Context Protocol server. Requires restart.</div>
656
+ <input type="text" id="general-mcp-addr" placeholder=":8081" style="width:160px;padding:4px 8px;font-size:12px">
657
+ </div>
658
+ </div>
659
+ <div id="general-save-result" style="display:none;margin:0 16px 12px"></div>
660
+ </div>
661
+
662
+ <!-- ergo -->
663
+ <div class="card" id="card-ergo">
664
+ <div class="card-header" onclick="toggleCard('card-ergo',event)">
665
+ <h2>IRC server (ergo)</h2><span class="card-desc">embedded IRC server settings</span><span class="collapse-icon">▾</span>
666
+ <div class="spacer"></div>
667
+ <button class="sm primary" onclick="event.stopPropagation();saveErgoConfig()">save</button>
668
+ </div>
669
+ <div class="card-body">
670
+ <div class="setting-row">
671
+ <div class="setting-label">network name</div>
672
+ <div class="setting-desc">Human-readable IRC network name. Requires restart.</div>
673
+ <input type="text" id="ergo-network-name" placeholder="scuttlebot" style="width:220px;padding:4px 8px;font-size:12px">
674
+ </div>
675
+ <div class="setting-row">
676
+ <div class="setting-label">server name</div>
677
+ <div class="setting-desc">IRC server hostname (e.g. irc.example.com). Requires restart.</div>
678
+ <input type="text" id="ergo-server-name" placeholder="irc.scuttlebot.local" style="width:220px;padding:4px 8px;font-size:12px">
679
+ </div>
680
+ <div class="setting-row">
681
+ <div class="setting-label">IRC address</div>
682
+ <div class="setting-desc">Address Ergo listens on for IRC connections. Requires restart.</div>
683
+ <input type="text" id="ergo-irc-addr" placeholder="127.0.0.1:6667" style="width:180px;padding:4px 8px;font-size:12px">
684
+ </div>
685
+ <div class="setting-row">
686
+ <div class="setting-label">external mode</div>
687
+ <div class="setting-desc">Disable subprocess management — scuttlebot expects Ergo to already be running. Requires restart.</div>
688
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
689
+ <input type="checkbox" id="ergo-external">
690
+ <span style="font-size:12px">external</span>
691
+ </label>
692
+ </div>
693
+ </div>
694
+ <div id="ergo-save-result" style="display:none;margin:0 16px 12px"></div>
695
+ </div>
696
+
697
+ <!-- TLS -->
698
+ <div class="card" id="card-tls-config">
699
+ <div class="card-header" onclick="toggleCard('card-tls-config',event)">
700
+ <h2>TLS / HTTPS</h2><span class="card-desc">HTTPS and Let's Encrypt configuration</span><span class="collapse-icon">▾</span>
701
+ <div class="spacer"></div>
702
+ <button class="sm primary" onclick="event.stopPropagation();saveTLSConfig()">save</button>
703
+ </div>
704
+ <div class="card-body">
705
+ <div class="setting-row">
706
+ <div class="setting-label">domain</div>
707
+ <div class="setting-desc">Domain for Let's Encrypt certificate. Leave blank for HTTP only. Requires restart.</div>
708
+ <input type="text" id="tls-domain" placeholder="scuttlebot.example.com" style="width:240px;padding:4px 8px;font-size:12px">
709
+ </div>
710
+ <div class="setting-row">
711
+ <div class="setting-label">email</div>
712
+ <div class="setting-desc">Sent to Let's Encrypt for expiry notifications.</div>
713
+ <input type="email" id="tls-email" placeholder="[email protected]" style="width:240px;padding:4px 8px;font-size:12px">
714
+ </div>
715
+ <div class="setting-row">
716
+ <div class="setting-label">allow insecure</div>
717
+ <div class="setting-desc">Keep HTTP running on :80 alongside HTTPS.</div>
718
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
719
+ <input type="checkbox" id="tls-allow-insecure">
720
+ <span style="font-size:12px">enabled</span>
721
+ </label>
722
+ </div>
723
+ </div>
724
+ <div id="tls-config-save-result" style="display:none;margin:0 16px 12px"></div>
725
+ </div>
726
+
727
+ <!-- topology -->
728
+ <div class="card" id="card-topology">
729
+ <div class="card-header" onclick="toggleCard('card-topology',event)">
730
+ <h2>topology</h2><span class="card-desc">static channels and prefix-based channel rules</span><span class="collapse-icon">▾</span>
731
+ <div class="spacer"></div>
732
+ <button class="sm" onclick="event.stopPropagation();loadConfigCards()">↺ refresh</button>
733
+ <button class="sm primary" onclick="event.stopPropagation();saveTopologyConfig()">save</button>
734
+ </div>
735
+ <div class="card-body">
736
+ <div class="setting-row">
737
+ <div class="setting-label">manager nick</div>
738
+ <div class="setting-desc">IRC nick used by the topology manager to register channels via ChanServ.</div>
739
+ <input type="text" id="topo-nick" placeholder="topology" style="width:160px;padding:4px 8px;font-size:12px">
740
+ </div>
741
+ <div class="setting-row">
742
+ <div class="setting-label">config history</div>
743
+ <div class="setting-desc">Number of scuttlebot.yaml snapshots to keep before pruning.</div>
744
+ <div style="display:flex;align-items:center;gap:6px">
745
+ <input type="number" id="topo-history-keep" placeholder="20" min="0" style="width:80px;padding:4px 8px;font-size:12px">
746
+ <span style="font-size:12px;color:#8b949e">snapshots</span>
747
+ </div>
748
+ </div>
749
+
750
+ <!-- static channels -->
751
+ <div style="margin-top:20px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
752
+ <strong style="font-size:13px">static channels</strong>
753
+ <span style="font-size:11px;color:#8b949e;flex:1">Provisioned at startup. ChanServ registers these channels and invites the listed bots.</span>
754
+ <button class="sm" onclick="topoAddStaticChannel()">+ add</button>
755
+ </div>
756
+ <div id="topo-static-channels">
757
+ <div class="empty-state" style="padding:12px;font-size:12px">no static channels configured</div>
758
+ </div>
759
+
760
+ <!-- channel types -->
761
+ <div style="margin-top:20px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
762
+ <strong style="font-size:13px">channel types</strong>
763
+ <span style="font-size:11px;color:#8b949e;flex:1">Prefix-based rules applied when agents create channels.</span>
764
+ <button class="sm" onclick="topoAddChannelType()">+ add</button>
765
+ </div>
766
+ <div id="topo-channel-types">
767
+ <div class="empty-state" style="padding:12px;font-size:12px">no channel types configured</div>
768
+ </div>
769
+
770
+ <div id="topo-save-result" style="display:none;margin-top:12px"></div>
771
+ </div>
772
+ </div>
609773
610774
<!-- about -->
611775
<div class="card">
612776
<div class="card-header" style="cursor:default"><h2>about</h2></div>
613777
<div class="card-body" style="font-size:13px;color:#8b949e;line-height:1.8">
@@ -629,11 +793,11 @@
629793
<div class="pane-inner">
630794
631795
<!-- LLM backends -->
632796
<div class="card" id="card-ai-backends">
633797
<div class="card-header" style="cursor:default">
634
- <h2>LLM backends</h2>
798
+ <h2>LLM backends</h2><span class="card-desc">configured providers for oracle and other LLM bots</span>
635799
<div class="spacer"></div>
636800
<button class="sm" onclick="loadAI()">↺ refresh</button>
637801
<button class="sm primary" onclick="openAddBackend()">+ add backend</button>
638802
</div>
639803
<div class="card-body" style="padding:0">
@@ -768,19 +932,19 @@
768932
</div>
769933
</div>
770934
771935
<!-- supported backends reference -->
772936
<div class="card" id="card-ai-supported">
773
- <div class="card-header" onclick="toggleCard('card-ai-supported',event)"><h2>supported backends</h2><span class="collapse-icon">▾</span></div>
937
+ <div class="card-header" onclick="toggleCard('card-ai-supported',event)"><h2>supported backends</h2><span class="card-desc">all available provider types</span><span class="collapse-icon">▾</span></div>
774938
<div class="card-body" id="ai-supported-list">
775939
<div class="empty-state">loading…</div>
776940
</div>
777941
</div>
778942
779943
<!-- config example -->
780944
<div class="card" id="card-ai-example" style="display:none">
781
- <div class="card-header" onclick="toggleCard('card-ai-example',event)"><h2>YAML example</h2><span class="collapse-icon">▾</span></div>
945
+ <div class="card-header" onclick="toggleCard('card-ai-example',event)"><h2>YAML example</h2><span class="card-desc">copy-paste starter config</span><span class="collapse-icon">▾</span></div>
782946
<div class="card-body">
783947
<pre style="font-size:12px;color:#a5d6ff;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:12px;overflow-x:auto;white-space:pre">llm:
784948
backends:
785949
- name: openai-main
786950
backend: openai
@@ -2321,10 +2485,11 @@
23212485
renderBehaviors(s.policies.behaviors || []);
23222486
renderAgentPolicy(s.policies.agent_policy || {});
23232487
renderBridgePolicy(s.policies.bridge || {});
23242488
renderLoggingPolicy(s.policies.logging || {});
23252489
loadAdmins();
2490
+ loadConfigCards();
23262491
} catch(e) {
23272492
document.getElementById('tls-badge').textContent = 'error';
23282493
}
23292494
}
23302495
@@ -2437,42 +2602,295 @@
24372602
const rot = document.getElementById('policy-log-rotation').value;
24382603
document.getElementById('policy-log-size-row').style.display = rot === 'size' ? '' : 'none';
24392604
}
24402605
24412606
async function savePolicies() {
2607
+ // Saves behaviors only — agent_policy, logging, and bridge are now
2608
+ // persisted to scuttlebot.yaml via PUT /v1/config.
24422609
if (!currentPolicies) return;
24432610
const p = JSON.parse(JSON.stringify(currentPolicies)); // deep copy
2444
- p.agent_policy = {
2445
- require_checkin: document.getElementById('policy-checkin-enabled').checked,
2446
- checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2447
- required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2448
- };
2449
- p.bridge = {
2450
- web_user_ttl_minutes: parseInt(document.getElementById('policy-bridge-web-user-ttl').value, 10) || 5,
2451
- };
2452
- p.logging = {
2453
- enabled: document.getElementById('policy-logging-enabled').checked,
2454
- dir: document.getElementById('policy-log-dir').value.trim(),
2455
- format: document.getElementById('policy-log-format').value,
2456
- rotation: document.getElementById('policy-log-rotation').value,
2457
- max_size_mb: parseInt(document.getElementById('policy-log-max-size').value, 10) || 0,
2458
- per_channel: document.getElementById('policy-log-per-channel').checked,
2459
- max_age_days: parseInt(document.getElementById('policy-log-max-age').value, 10) || 0,
2460
- };
24612611
const resultEl = document.getElementById('policies-save-result');
24622612
try {
24632613
currentPolicies = await api('PUT', '/v1/settings/policies', p);
24642614
resultEl.style.display = 'block';
2465
- resultEl.innerHTML = renderAlert('success', 'Settings saved.');
2615
+ resultEl.innerHTML = renderAlert('success', 'Behaviors saved.');
24662616
setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
24672617
} catch(e) {
24682618
resultEl.style.display = 'block';
24692619
resultEl.innerHTML = renderAlert('error', e.message);
24702620
}
24712621
}
2622
+
2623
+// --- topology config ---
2624
+let _topoChannels = [];
2625
+let _topoTypes = [];
2626
+
2627
+function renderTopoStaticChannels() {
2628
+ const el = document.getElementById('topo-static-channels');
2629
+ if (!_topoChannels.length) {
2630
+ el.innerHTML = '<div class="empty-state" style="padding:12px;font-size:12px">no static channels configured</div>';
2631
+ return;
2632
+ }
2633
+ el.innerHTML = `
2634
+ <table style="width:100%;font-size:12px;border-collapse:collapse">
2635
+ <thead>
2636
+ <tr style="color:#8b949e;border-bottom:1px solid #30363d">
2637
+ <th style="text-align:left;padding:4px 8px;font-weight:500">name</th>
2638
+ <th style="text-align:left;padding:4px 8px;font-weight:500">topic</th>
2639
+ <th style="text-align:left;padding:4px 8px;font-weight:500">autojoin</th>
2640
+ <th style="padding:4px 8px"></th>
2641
+ </tr>
2642
+ </thead>
2643
+ <tbody>
2644
+ ${_topoChannels.map((c, i) => `
2645
+ <tr style="border-bottom:1px solid #21262d">
2646
+ <td style="padding:4px 8px">
2647
+ <input type="text" value="${esc(c.name||'')}" style="width:120px;padding:2px 6px;font-size:11px"
2648
+ onchange="_topoChannels[${i}].name=this.value">
2649
+ </td>
2650
+ <td style="padding:4px 8px">
2651
+ <input type="text" value="${esc(c.topic||'')}" style="width:160px;padding:2px 6px;font-size:11px"
2652
+ onchange="_topoChannels[${i}].topic=this.value">
2653
+ </td>
2654
+ <td style="padding:4px 8px">
2655
+ <input type="text" value="${esc((c.autojoin||[]).join(', '))}" placeholder="bridge, sentinel"
2656
+ style="width:160px;padding:2px 6px;font-size:11px"
2657
+ onchange="_topoChannels[${i}].autojoin=this.value.split(',').map(s=>s.trim()).filter(Boolean)">
2658
+ </td>
2659
+ <td style="padding:4px 8px;text-align:right">
2660
+ <button class="sm" onclick="topoDeleteStaticChannel(${i})">✕</button>
2661
+ </td>
2662
+ </tr>
2663
+ `).join('')}
2664
+ </tbody>
2665
+ </table>`;
2666
+}
2667
+
2668
+function renderTopoChannelTypes() {
2669
+ const el = document.getElementById('topo-channel-types');
2670
+ if (!_topoTypes.length) {
2671
+ el.innerHTML = '<div class="empty-state" style="padding:12px;font-size:12px">no channel types configured</div>';
2672
+ return;
2673
+ }
2674
+ el.innerHTML = `
2675
+ <table style="width:100%;font-size:12px;border-collapse:collapse">
2676
+ <thead>
2677
+ <tr style="color:#8b949e;border-bottom:1px solid #30363d">
2678
+ <th style="text-align:left;padding:4px 8px;font-weight:500">name</th>
2679
+ <th style="text-align:left;padding:4px 8px;font-weight:500">prefix</th>
2680
+ <th style="text-align:left;padding:4px 8px;font-weight:500">ttl</th>
2681
+ <th style="text-align:left;padding:4px 8px;font-weight:500">ephemeral</th>
2682
+ <th style="padding:4px 8px"></th>
2683
+ </tr>
2684
+ </thead>
2685
+ <tbody>
2686
+ ${_topoTypes.map((x, i) => `
2687
+ <tr style="border-bottom:1px solid #21262d">
2688
+ <td style="padding:4px 8px">
2689
+ <input type="text" value="${esc(x.name||'')}" style="width:100px;padding:2px 6px;font-size:11px"
2690
+ onchange="_topoTypes[${i}].name=this.value">
2691
+ </td>
2692
+ <td style="padding:4px 8px">
2693
+ <input type="text" value="${esc(x.prefix||'')}" placeholder="task." style="width:100px;padding:2px 6px;font-size:11px"
2694
+ onchange="_topoTypes[${i}].prefix=this.value">
2695
+ </td>
2696
+ <td style="padding:4px 8px">
2697
+ <input type="text" value="${esc(x.ttl||'')}" placeholder="24h" style="width:80px;padding:2px 6px;font-size:11px"
2698
+ title="Duration string e.g. 1h, 24h, 72h"
2699
+ onchange="_topoTypes[${i}].ttl=this.value">
2700
+ </td>
2701
+ <td style="padding:4px 8px;text-align:center">
2702
+ <input type="checkbox" ${x.ephemeral ? 'checked' : ''} style="width:auto"
2703
+ onchange="_topoTypes[${i}].ephemeral=this.checked">
2704
+ </td>
2705
+ <td style="padding:4px 8px;text-align:right">
2706
+ <button class="sm" onclick="topoDeleteChannelType(${i})">✕</button>
2707
+ </td>
2708
+ </tr>
2709
+ `).join('')}
2710
+ </tbody>
2711
+ </table>`;
2712
+}
2713
+
2714
+function topoAddStaticChannel() {
2715
+ _topoChannels.push({name: '', topic: '', autojoin: []});
2716
+ renderTopoStaticChannels();
2717
+ // focus the last name input
2718
+ const rows = document.querySelectorAll('#topo-static-channels tbody tr');
2719
+ if (rows.length) rows[rows.length-1].querySelector('input').focus();
2720
+}
2721
+
2722
+function topoAddChannelType() {
2723
+ _topoTypes.push({name: '', prefix: '', ephemeral: false, ttl: ''});
2724
+ renderTopoChannelTypes();
2725
+ const rows = document.querySelectorAll('#topo-channel-types tbody tr');
2726
+ if (rows.length) rows[rows.length-1].querySelector('input').focus();
2727
+}
2728
+
2729
+function topoDeleteStaticChannel(idx) {
2730
+ _topoChannels.splice(idx, 1);
2731
+ renderTopoStaticChannels();
2732
+}
2733
+
2734
+function topoDeleteChannelType(idx) {
2735
+ _topoTypes.splice(idx, 1);
2736
+ renderTopoChannelTypes();
2737
+}
2738
+
2739
+async function saveTopologyConfig() {
2740
+ const resultEl = document.getElementById('topo-save-result');
2741
+ const channels = _topoChannels.filter(c => c.name.trim());
2742
+ const types = _topoTypes.filter(x => x.name.trim() && x.prefix.trim());
2743
+ const payload = {
2744
+ topology: {
2745
+ nick: document.getElementById('topo-nick').value.trim() || 'topology',
2746
+ channels: channels,
2747
+ types: types,
2748
+ },
2749
+ config_history: {
2750
+ keep: parseInt(document.getElementById('topo-history-keep').value, 10) || 20,
2751
+ },
2752
+ };
2753
+ try {
2754
+ const res = await api('PUT', '/v1/config', payload);
2755
+ resultEl.style.display = 'block';
2756
+ let msg = 'Topology config saved.';
2757
+ if (res.restart_required && res.restart_required.length) {
2758
+ msg += ' Restart required for: ' + res.restart_required.join(', ') + '.';
2759
+ }
2760
+ resultEl.innerHTML = renderAlert('success', msg);
2761
+ setTimeout(() => { resultEl.style.display = 'none'; }, 4000);
2762
+ } catch(e) {
2763
+ resultEl.style.display = 'block';
2764
+ resultEl.innerHTML = renderAlert('error', e.message);
2765
+ }
2766
+}
2767
+
2768
+// --- shared config save helper ---
2769
+async function saveConfigPatch(patch, resultElId) {
2770
+ const resultEl = document.getElementById(resultElId);
2771
+ try {
2772
+ const res = await api('PUT', '/v1/config', patch);
2773
+ let msg = 'Saved.';
2774
+ if (res.restart_required && res.restart_required.length) {
2775
+ msg += ' Restart required for: ' + res.restart_required.join(', ') + '.';
2776
+ }
2777
+ resultEl.style.display = 'block';
2778
+ resultEl.innerHTML = renderAlert('success', msg);
2779
+ setTimeout(() => { resultEl.style.display = 'none'; }, 4000);
2780
+ } catch(e) {
2781
+ resultEl.style.display = 'block';
2782
+ resultEl.innerHTML = renderAlert('error', e.message);
2783
+ }
2784
+}
2785
+
2786
+// --- config-backed cards ---
2787
+async function loadConfigCards() {
2788
+ try {
2789
+ const cfg = await api('GET', '/v1/config');
2790
+ // general
2791
+ document.getElementById('general-api-addr').value = cfg.api_addr || '';
2792
+ document.getElementById('general-mcp-addr').value = cfg.mcp_addr || '';
2793
+ // ergo
2794
+ const e = cfg.ergo || {};
2795
+ document.getElementById('ergo-network-name').value = e.network_name || '';
2796
+ document.getElementById('ergo-server-name').value = e.server_name || '';
2797
+ document.getElementById('ergo-irc-addr').value = e.irc_addr || '';
2798
+ document.getElementById('ergo-external').checked = !!e.external;
2799
+ // tls
2800
+ const t = cfg.tls || {};
2801
+ document.getElementById('tls-domain').value = t.domain || '';
2802
+ document.getElementById('tls-email').value = t.email || '';
2803
+ document.getElementById('tls-allow-insecure').checked = !!t.allow_insecure;
2804
+ // bridge (full)
2805
+ const b = cfg.bridge || {};
2806
+ document.getElementById('bridge-enabled').checked = b.enabled !== false;
2807
+ document.getElementById('bridge-nick').value = b.nick || '';
2808
+ document.getElementById('bridge-channels').value = (b.channels || []).join(', ');
2809
+ document.getElementById('bridge-buffer-size').value = b.buffer_size || '';
2810
+ document.getElementById('policy-bridge-web-user-ttl').value = b.web_user_ttl_minutes || 5;
2811
+ // topology + history
2812
+ const topo = cfg.topology || {};
2813
+ const h = cfg.config_history || {};
2814
+ _topoChannels = (topo.channels || []).map(c => Object.assign({}, c));
2815
+ _topoTypes = (topo.types || []).map(x => Object.assign({}, x));
2816
+ document.getElementById('topo-nick').value = topo.nick || 'topology';
2817
+ document.getElementById('topo-history-keep').value = h.keep != null ? h.keep : 20;
2818
+ renderTopoStaticChannels();
2819
+ renderTopoChannelTypes();
2820
+ } catch(e) {
2821
+ console.error('loadConfigCards:', e);
2822
+ }
2823
+}
2824
+
2825
+function saveAgentPolicy() {
2826
+ saveConfigPatch({
2827
+ agent_policy: {
2828
+ require_checkin: document.getElementById('policy-checkin-enabled').checked,
2829
+ checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2830
+ required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2831
+ }
2832
+ }, 'agentpolicy-save-result');
2833
+}
2834
+
2835
+function saveBridgeConfig() {
2836
+ saveConfigPatch({
2837
+ bridge: {
2838
+ enabled: document.getElementById('bridge-enabled').checked,
2839
+ nick: document.getElementById('bridge-nick').value.trim() || undefined,
2840
+ channels: document.getElementById('bridge-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2841
+ buffer_size: parseInt(document.getElementById('bridge-buffer-size').value, 10) || undefined,
2842
+ web_user_ttl_minutes: parseInt(document.getElementById('policy-bridge-web-user-ttl').value, 10) || 5,
2843
+ }
2844
+ }, 'bridge-save-result');
2845
+}
2846
+
2847
+function saveLogging() {
2848
+ saveConfigPatch({
2849
+ logging: {
2850
+ enabled: document.getElementById('policy-logging-enabled').checked,
2851
+ dir: document.getElementById('policy-log-dir').value.trim(),
2852
+ format: document.getElementById('policy-log-format').value,
2853
+ rotation: document.getElementById('policy-log-rotation').value,
2854
+ max_size_mb: parseInt(document.getElementById('policy-log-max-size').value, 10) || 0,
2855
+ per_channel: document.getElementById('policy-log-per-channel').checked,
2856
+ max_age_days: parseInt(document.getElementById('policy-log-max-age').value, 10) || 0,
2857
+ }
2858
+ }, 'logging-save-result');
2859
+}
2860
+
2861
+function saveGeneralConfig() {
2862
+ const patch = {};
2863
+ const addr = document.getElementById('general-api-addr').value.trim();
2864
+ const mcp = document.getElementById('general-mcp-addr').value.trim();
2865
+ if (addr) patch.api_addr = addr;
2866
+ if (mcp) patch.mcp_addr = mcp;
2867
+ saveConfigPatch(patch, 'general-save-result');
2868
+}
2869
+
2870
+function saveErgoConfig() {
2871
+ saveConfigPatch({
2872
+ ergo: {
2873
+ network_name: document.getElementById('ergo-network-name').value.trim() || undefined,
2874
+ server_name: document.getElementById('ergo-server-name').value.trim() || undefined,
2875
+ irc_addr: document.getElementById('ergo-irc-addr').value.trim() || undefined,
2876
+ external: document.getElementById('ergo-external').checked,
2877
+ }
2878
+ }, 'ergo-save-result');
2879
+}
2880
+
2881
+function saveTLSConfig() {
2882
+ saveConfigPatch({
2883
+ tls: {
2884
+ domain: document.getElementById('tls-domain').value.trim() || undefined,
2885
+ email: document.getElementById('tls-email').value.trim() || undefined,
2886
+ allow_insecure: document.getElementById('tls-allow-insecure').checked,
2887
+ }
2888
+ }, 'tls-config-save-result');
2889
+}
24722890
24732891
// --- init ---
24742892
function loadAll() { loadStatus(); loadAgents(); loadSettings(); startMetricsPoll(); }
24752893
initAuth();
24762894
</script>
24772895
</body>
24782896
</html>
24792897
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -29,10 +29,11 @@
29 /* cards */
30 .card { background:#161b22; border:1px solid #30363d; border-radius:8px; overflow:hidden; }
31 .card-header { padding:12px 16px; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
32 .card-header:hover { background:#1c2128; }
33 .card-header h2 { font-size:14px; font-weight:600; }
 
34 .card-header .collapse-icon { font-size:11px; color:#8b949e; margin-left:2px; transition:transform .15s; }
35 .card.collapsed .card-header { border-bottom:none; }
36 .card.collapsed .card-body { display:none; }
37 .card.collapsed .collapse-icon { transform:rotate(-90deg); }
38 .card-body { padding:16px; }
@@ -430,11 +431,11 @@
430 <div class="tab-pane" id="pane-settings">
431 <div class="pane-inner">
432
433 <!-- connection -->
434 <div class="card" id="card-connection">
435 <div class="card-header" style="cursor:default"><h2>connection</h2></div>
436 <div class="card-body">
437 <div class="setting-row">
438 <div class="setting-label">signed in as</div>
439 <div class="setting-desc">Current admin session.</div>
440 <code class="setting-val" id="settings-username-display">—</code>
@@ -458,11 +459,11 @@
458 </div>
459 </div>
460
461 <!-- admin accounts -->
462 <div class="card" id="card-admins">
463 <div class="card-header" onclick="toggleCard('card-admins',event)"><h2>admin accounts</h2><span class="collapse-icon">▾</span></div>
464 <div id="admins-list-container"></div>
465 <div class="card-body" style="border-top:1px solid #21262d">
466 <p style="font-size:12px;color:#8b949e;margin-bottom:12px">Add an admin account. Admins sign in at the login screen with username + password.</p>
467 <form id="add-admin-form" onsubmit="addAdmin(event)" style="flex-direction:row;align-items:flex-end;gap:10px;flex-wrap:wrap">
468 <div style="flex:1;min-width:130px"><label>username</label><input type="text" id="new-admin-username" autocomplete="off"></div>
@@ -473,11 +474,11 @@
473 </div>
474 </div>
475
476 <!-- tls -->
477 <div class="card" id="card-tls">
478 <div class="card-header" onclick="toggleCard('card-tls',event)"><h2>TLS / SSL</h2><span class="collapse-icon">▾</span><div class="spacer"></div><span id="tls-badge" class="badge">loading…</span></div>
479 <div class="card-body">
480 <div id="tls-status-rows"></div>
481 <div class="alert info" style="margin-top:12px;font-size:12px">
482 <span class="icon">ℹ</span>
483 <span>TLS is configured in <code style="color:#a5d6ff">scuttlebot.yaml</code> under <code style="color:#a5d6ff">tls:</code>.
@@ -487,11 +488,11 @@
487 </div>
488
489 <!-- system behaviors -->
490 <div class="card" id="card-behaviors">
491 <div class="card-header" onclick="toggleCard('card-behaviors',event)">
492 <h2>system behaviors</h2><span class="collapse-icon">▾</span>
493 <div class="spacer"></div>
494 <button class="sm primary" onclick="savePolicies()">save</button>
495 </div>
496 <div class="card-body" style="padding:0">
497 <div id="behaviors-list"></div>
@@ -498,11 +499,11 @@
498 </div>
499 </div>
500
501 <!-- agent policy -->
502 <div class="card" id="card-agentpolicy">
503 <div class="card-header" onclick="toggleCard('card-agentpolicy',event)"><h2>agent policy</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
504 <div class="card-body">
505 <div class="setting-row">
506 <div class="setting-label">require check-in</div>
507 <div class="setting-desc">Agents must join a coordination channel before others.</div>
508 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
@@ -519,30 +520,58 @@
519 <div class="setting-label">required channels</div>
520 <div class="setting-desc">Channels every agent is added to automatically.</div>
521 <input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
522 </div>
523 </div>
 
524 </div>
525
526 <!-- bridge -->
527 <div class="card" id="card-bridgepolicy">
528 <div class="card-header" onclick="toggleCard('card-bridgepolicy',event)"><h2>web bridge</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
529 <div class="card-body">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530 <div class="setting-row">
531 <div class="setting-label">web user TTL</div>
532 <div class="setting-desc">How long HTTP-posted nicks stay visible in the channel user list after their last message.</div>
533 <div style="display:flex;align-items:center;gap:6px">
534 <input type="number" id="policy-bridge-web-user-ttl" placeholder="5" min="1" style="width:80px;padding:4px 8px;font-size:12px">
535 <span style="font-size:12px;color:#8b949e">minutes</span>
536 </div>
537 </div>
538 </div>
 
539 </div>
540
541 <!-- logging -->
542 <div class="card" id="card-logging">
543 <div class="card-header" onclick="toggleCard('card-logging',event)"><h2>message logging</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
544 <div class="card-body">
545 <div class="setting-row">
546 <div class="setting-label">enabled</div>
547 <div class="setting-desc">Write every channel message to disk.</div>
548 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
@@ -601,13 +630,148 @@
601 <span style="font-size:12px;color:#8b949e">days</span>
602 </div>
603 </div>
604 </div>
605 </div>
 
606 </div>
607
608 <div id="policies-save-result" style="display:none"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
610 <!-- about -->
611 <div class="card">
612 <div class="card-header" style="cursor:default"><h2>about</h2></div>
613 <div class="card-body" style="font-size:13px;color:#8b949e;line-height:1.8">
@@ -629,11 +793,11 @@
629 <div class="pane-inner">
630
631 <!-- LLM backends -->
632 <div class="card" id="card-ai-backends">
633 <div class="card-header" style="cursor:default">
634 <h2>LLM backends</h2>
635 <div class="spacer"></div>
636 <button class="sm" onclick="loadAI()">↺ refresh</button>
637 <button class="sm primary" onclick="openAddBackend()">+ add backend</button>
638 </div>
639 <div class="card-body" style="padding:0">
@@ -768,19 +932,19 @@
768 </div>
769 </div>
770
771 <!-- supported backends reference -->
772 <div class="card" id="card-ai-supported">
773 <div class="card-header" onclick="toggleCard('card-ai-supported',event)"><h2>supported backends</h2><span class="collapse-icon">▾</span></div>
774 <div class="card-body" id="ai-supported-list">
775 <div class="empty-state">loading…</div>
776 </div>
777 </div>
778
779 <!-- config example -->
780 <div class="card" id="card-ai-example" style="display:none">
781 <div class="card-header" onclick="toggleCard('card-ai-example',event)"><h2>YAML example</h2><span class="collapse-icon">▾</span></div>
782 <div class="card-body">
783 <pre style="font-size:12px;color:#a5d6ff;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:12px;overflow-x:auto;white-space:pre">llm:
784 backends:
785 - name: openai-main
786 backend: openai
@@ -2321,10 +2485,11 @@
2321 renderBehaviors(s.policies.behaviors || []);
2322 renderAgentPolicy(s.policies.agent_policy || {});
2323 renderBridgePolicy(s.policies.bridge || {});
2324 renderLoggingPolicy(s.policies.logging || {});
2325 loadAdmins();
 
2326 } catch(e) {
2327 document.getElementById('tls-badge').textContent = 'error';
2328 }
2329 }
2330
@@ -2437,42 +2602,295 @@
2437 const rot = document.getElementById('policy-log-rotation').value;
2438 document.getElementById('policy-log-size-row').style.display = rot === 'size' ? '' : 'none';
2439 }
2440
2441 async function savePolicies() {
 
 
2442 if (!currentPolicies) return;
2443 const p = JSON.parse(JSON.stringify(currentPolicies)); // deep copy
2444 p.agent_policy = {
2445 require_checkin: document.getElementById('policy-checkin-enabled').checked,
2446 checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2447 required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2448 };
2449 p.bridge = {
2450 web_user_ttl_minutes: parseInt(document.getElementById('policy-bridge-web-user-ttl').value, 10) || 5,
2451 };
2452 p.logging = {
2453 enabled: document.getElementById('policy-logging-enabled').checked,
2454 dir: document.getElementById('policy-log-dir').value.trim(),
2455 format: document.getElementById('policy-log-format').value,
2456 rotation: document.getElementById('policy-log-rotation').value,
2457 max_size_mb: parseInt(document.getElementById('policy-log-max-size').value, 10) || 0,
2458 per_channel: document.getElementById('policy-log-per-channel').checked,
2459 max_age_days: parseInt(document.getElementById('policy-log-max-age').value, 10) || 0,
2460 };
2461 const resultEl = document.getElementById('policies-save-result');
2462 try {
2463 currentPolicies = await api('PUT', '/v1/settings/policies', p);
2464 resultEl.style.display = 'block';
2465 resultEl.innerHTML = renderAlert('success', 'Settings saved.');
2466 setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
2467 } catch(e) {
2468 resultEl.style.display = 'block';
2469 resultEl.innerHTML = renderAlert('error', e.message);
2470 }
2471 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2472
2473 // --- init ---
2474 function loadAll() { loadStatus(); loadAgents(); loadSettings(); startMetricsPoll(); }
2475 initAuth();
2476 </script>
2477 </body>
2478 </html>
2479
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -29,10 +29,11 @@
29 /* cards */
30 .card { background:#161b22; border:1px solid #30363d; border-radius:8px; overflow:hidden; }
31 .card-header { padding:12px 16px; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
32 .card-header:hover { background:#1c2128; }
33 .card-header h2 { font-size:14px; font-weight:600; }
34 .card-header .card-desc { font-size:11px; color:#6e7681; font-weight:400; }
35 .card-header .collapse-icon { font-size:11px; color:#8b949e; margin-left:2px; transition:transform .15s; }
36 .card.collapsed .card-header { border-bottom:none; }
37 .card.collapsed .card-body { display:none; }
38 .card.collapsed .collapse-icon { transform:rotate(-90deg); }
39 .card-body { padding:16px; }
@@ -430,11 +431,11 @@
431 <div class="tab-pane" id="pane-settings">
432 <div class="pane-inner">
433
434 <!-- connection -->
435 <div class="card" id="card-connection">
436 <div class="card-header" style="cursor:default"><h2>connection</h2><span class="card-desc">current session and server endpoints</span></div>
437 <div class="card-body">
438 <div class="setting-row">
439 <div class="setting-label">signed in as</div>
440 <div class="setting-desc">Current admin session.</div>
441 <code class="setting-val" id="settings-username-display">—</code>
@@ -458,11 +459,11 @@
459 </div>
460 </div>
461
462 <!-- admin accounts -->
463 <div class="card" id="card-admins">
464 <div class="card-header" onclick="toggleCard('card-admins',event)"><h2>admin accounts</h2><span class="card-desc">who can sign in to this UI</span><span class="collapse-icon">▾</span></div>
465 <div id="admins-list-container"></div>
466 <div class="card-body" style="border-top:1px solid #21262d">
467 <p style="font-size:12px;color:#8b949e;margin-bottom:12px">Add an admin account. Admins sign in at the login screen with username + password.</p>
468 <form id="add-admin-form" onsubmit="addAdmin(event)" style="flex-direction:row;align-items:flex-end;gap:10px;flex-wrap:wrap">
469 <div style="flex:1;min-width:130px"><label>username</label><input type="text" id="new-admin-username" autocomplete="off"></div>
@@ -473,11 +474,11 @@
474 </div>
475 </div>
476
477 <!-- tls -->
478 <div class="card" id="card-tls">
479 <div class="card-header" onclick="toggleCard('card-tls',event)"><h2>TLS / SSL</h2><span class="card-desc">certificate status</span><span class="collapse-icon">▾</span><div class="spacer"></div><span id="tls-badge" class="badge">loading…</span></div>
480 <div class="card-body">
481 <div id="tls-status-rows"></div>
482 <div class="alert info" style="margin-top:12px;font-size:12px">
483 <span class="icon">ℹ</span>
484 <span>TLS is configured in <code style="color:#a5d6ff">scuttlebot.yaml</code> under <code style="color:#a5d6ff">tls:</code>.
@@ -487,11 +488,11 @@
488 </div>
489
490 <!-- system behaviors -->
491 <div class="card" id="card-behaviors">
492 <div class="card-header" onclick="toggleCard('card-behaviors',event)">
493 <h2>system behaviors</h2><span class="card-desc">bot toggles, rate limits, and default channel</span><span class="collapse-icon">▾</span>
494 <div class="spacer"></div>
495 <button class="sm primary" onclick="savePolicies()">save</button>
496 </div>
497 <div class="card-body" style="padding:0">
498 <div id="behaviors-list"></div>
@@ -498,11 +499,11 @@
499 </div>
500 </div>
501
502 <!-- agent policy -->
503 <div class="card" id="card-agentpolicy">
504 <div class="card-header" onclick="toggleCard('card-agentpolicy',event)"><h2>agent policy</h2><span class="card-desc">autojoin and check-in rules for all agents</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveAgentPolicy()">save</button></div>
505 <div class="card-body">
506 <div class="setting-row">
507 <div class="setting-label">require check-in</div>
508 <div class="setting-desc">Agents must join a coordination channel before others.</div>
509 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
@@ -519,30 +520,58 @@
520 <div class="setting-label">required channels</div>
521 <div class="setting-desc">Channels every agent is added to automatically.</div>
522 <input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
523 </div>
524 </div>
525 <div id="agentpolicy-save-result" style="display:none;margin:0 16px 12px"></div>
526 </div>
527
528 <!-- bridge -->
529 <div class="card" id="card-bridgepolicy">
530 <div class="card-header" onclick="toggleCard('card-bridgepolicy',event)"><h2>web bridge</h2><span class="card-desc">IRC bot that powers the web chat UI</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveBridgeConfig()">save</button></div>
531 <div class="card-body">
532 <div class="setting-row">
533 <div class="setting-label">enabled</div>
534 <div class="setting-desc">Start the bridge bot that powers the web chat UI.</div>
535 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
536 <input type="checkbox" id="bridge-enabled">
537 <span style="font-size:12px">enabled</span>
538 </label>
539 </div>
540 <div class="setting-row">
541 <div class="setting-label">nick</div>
542 <div class="setting-desc">IRC nick for the bridge bot. Requires restart.</div>
543 <input type="text" id="bridge-nick" placeholder="bridge" style="width:160px;padding:4px 8px;font-size:12px">
544 </div>
545 <div class="setting-row">
546 <div class="setting-label">channels</div>
547 <div class="setting-desc">Channels the bridge joins at startup.</div>
548 <input type="text" id="bridge-channels" placeholder="#general, #fleet" style="width:280px;padding:4px 8px;font-size:12px">
549 </div>
550 <div class="setting-row">
551 <div class="setting-label">message buffer</div>
552 <div class="setting-desc">Messages to keep per channel in memory.</div>
553 <div style="display:flex;align-items:center;gap:6px">
554 <input type="number" id="bridge-buffer-size" placeholder="200" min="1" style="width:80px;padding:4px 8px;font-size:12px">
555 <span style="font-size:12px;color:#8b949e">messages</span>
556 </div>
557 </div>
558 <div class="setting-row">
559 <div class="setting-label">web user TTL</div>
560 <div class="setting-desc">How long HTTP-posted nicks stay visible in the channel user list after their last message.</div>
561 <div style="display:flex;align-items:center;gap:6px">
562 <input type="number" id="policy-bridge-web-user-ttl" placeholder="5" min="1" style="width:80px;padding:4px 8px;font-size:12px">
563 <span style="font-size:12px;color:#8b949e">minutes</span>
564 </div>
565 </div>
566 </div>
567 <div id="bridge-save-result" style="display:none;margin:0 16px 12px"></div>
568 </div>
569
570 <!-- logging -->
571 <div class="card" id="card-logging">
572 <div class="card-header" onclick="toggleCard('card-logging',event)"><h2>message logging</h2><span class="card-desc">write channel traffic to disk</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveLogging()">save</button></div>
573 <div class="card-body">
574 <div class="setting-row">
575 <div class="setting-label">enabled</div>
576 <div class="setting-desc">Write every channel message to disk.</div>
577 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
@@ -601,13 +630,148 @@
630 <span style="font-size:12px;color:#8b949e">days</span>
631 </div>
632 </div>
633 </div>
634 </div>
635 <div id="logging-save-result" style="display:none;margin:0 16px 12px"></div>
636 </div>
637
638 <div id="policies-save-result" style="display:none"></div>
639
640 <!-- general -->
641 <div class="card" id="card-general">
642 <div class="card-header" onclick="toggleCard('card-general',event)">
643 <h2>general</h2><span class="card-desc">API and MCP server addresses</span><span class="collapse-icon">▾</span>
644 <div class="spacer"></div>
645 <button class="sm primary" onclick="event.stopPropagation();saveGeneralConfig()">save</button>
646 </div>
647 <div class="card-body">
648 <div class="setting-row">
649 <div class="setting-label">API address</div>
650 <div class="setting-desc">Address scuttlebot listens on for HTTP API requests. Requires restart.</div>
651 <input type="text" id="general-api-addr" placeholder=":8080" style="width:160px;padding:4px 8px;font-size:12px">
652 </div>
653 <div class="setting-row">
654 <div class="setting-label">MCP address</div>
655 <div class="setting-desc">Address for the Model Context Protocol server. Requires restart.</div>
656 <input type="text" id="general-mcp-addr" placeholder=":8081" style="width:160px;padding:4px 8px;font-size:12px">
657 </div>
658 </div>
659 <div id="general-save-result" style="display:none;margin:0 16px 12px"></div>
660 </div>
661
662 <!-- ergo -->
663 <div class="card" id="card-ergo">
664 <div class="card-header" onclick="toggleCard('card-ergo',event)">
665 <h2>IRC server (ergo)</h2><span class="card-desc">embedded IRC server settings</span><span class="collapse-icon">▾</span>
666 <div class="spacer"></div>
667 <button class="sm primary" onclick="event.stopPropagation();saveErgoConfig()">save</button>
668 </div>
669 <div class="card-body">
670 <div class="setting-row">
671 <div class="setting-label">network name</div>
672 <div class="setting-desc">Human-readable IRC network name. Requires restart.</div>
673 <input type="text" id="ergo-network-name" placeholder="scuttlebot" style="width:220px;padding:4px 8px;font-size:12px">
674 </div>
675 <div class="setting-row">
676 <div class="setting-label">server name</div>
677 <div class="setting-desc">IRC server hostname (e.g. irc.example.com). Requires restart.</div>
678 <input type="text" id="ergo-server-name" placeholder="irc.scuttlebot.local" style="width:220px;padding:4px 8px;font-size:12px">
679 </div>
680 <div class="setting-row">
681 <div class="setting-label">IRC address</div>
682 <div class="setting-desc">Address Ergo listens on for IRC connections. Requires restart.</div>
683 <input type="text" id="ergo-irc-addr" placeholder="127.0.0.1:6667" style="width:180px;padding:4px 8px;font-size:12px">
684 </div>
685 <div class="setting-row">
686 <div class="setting-label">external mode</div>
687 <div class="setting-desc">Disable subprocess management — scuttlebot expects Ergo to already be running. Requires restart.</div>
688 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
689 <input type="checkbox" id="ergo-external">
690 <span style="font-size:12px">external</span>
691 </label>
692 </div>
693 </div>
694 <div id="ergo-save-result" style="display:none;margin:0 16px 12px"></div>
695 </div>
696
697 <!-- TLS -->
698 <div class="card" id="card-tls-config">
699 <div class="card-header" onclick="toggleCard('card-tls-config',event)">
700 <h2>TLS / HTTPS</h2><span class="card-desc">HTTPS and Let's Encrypt configuration</span><span class="collapse-icon">▾</span>
701 <div class="spacer"></div>
702 <button class="sm primary" onclick="event.stopPropagation();saveTLSConfig()">save</button>
703 </div>
704 <div class="card-body">
705 <div class="setting-row">
706 <div class="setting-label">domain</div>
707 <div class="setting-desc">Domain for Let's Encrypt certificate. Leave blank for HTTP only. Requires restart.</div>
708 <input type="text" id="tls-domain" placeholder="scuttlebot.example.com" style="width:240px;padding:4px 8px;font-size:12px">
709 </div>
710 <div class="setting-row">
711 <div class="setting-label">email</div>
712 <div class="setting-desc">Sent to Let's Encrypt for expiry notifications.</div>
713 <input type="email" id="tls-email" placeholder="[email protected]" style="width:240px;padding:4px 8px;font-size:12px">
714 </div>
715 <div class="setting-row">
716 <div class="setting-label">allow insecure</div>
717 <div class="setting-desc">Keep HTTP running on :80 alongside HTTPS.</div>
718 <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
719 <input type="checkbox" id="tls-allow-insecure">
720 <span style="font-size:12px">enabled</span>
721 </label>
722 </div>
723 </div>
724 <div id="tls-config-save-result" style="display:none;margin:0 16px 12px"></div>
725 </div>
726
727 <!-- topology -->
728 <div class="card" id="card-topology">
729 <div class="card-header" onclick="toggleCard('card-topology',event)">
730 <h2>topology</h2><span class="card-desc">static channels and prefix-based channel rules</span><span class="collapse-icon">▾</span>
731 <div class="spacer"></div>
732 <button class="sm" onclick="event.stopPropagation();loadConfigCards()">↺ refresh</button>
733 <button class="sm primary" onclick="event.stopPropagation();saveTopologyConfig()">save</button>
734 </div>
735 <div class="card-body">
736 <div class="setting-row">
737 <div class="setting-label">manager nick</div>
738 <div class="setting-desc">IRC nick used by the topology manager to register channels via ChanServ.</div>
739 <input type="text" id="topo-nick" placeholder="topology" style="width:160px;padding:4px 8px;font-size:12px">
740 </div>
741 <div class="setting-row">
742 <div class="setting-label">config history</div>
743 <div class="setting-desc">Number of scuttlebot.yaml snapshots to keep before pruning.</div>
744 <div style="display:flex;align-items:center;gap:6px">
745 <input type="number" id="topo-history-keep" placeholder="20" min="0" style="width:80px;padding:4px 8px;font-size:12px">
746 <span style="font-size:12px;color:#8b949e">snapshots</span>
747 </div>
748 </div>
749
750 <!-- static channels -->
751 <div style="margin-top:20px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
752 <strong style="font-size:13px">static channels</strong>
753 <span style="font-size:11px;color:#8b949e;flex:1">Provisioned at startup. ChanServ registers these channels and invites the listed bots.</span>
754 <button class="sm" onclick="topoAddStaticChannel()">+ add</button>
755 </div>
756 <div id="topo-static-channels">
757 <div class="empty-state" style="padding:12px;font-size:12px">no static channels configured</div>
758 </div>
759
760 <!-- channel types -->
761 <div style="margin-top:20px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
762 <strong style="font-size:13px">channel types</strong>
763 <span style="font-size:11px;color:#8b949e;flex:1">Prefix-based rules applied when agents create channels.</span>
764 <button class="sm" onclick="topoAddChannelType()">+ add</button>
765 </div>
766 <div id="topo-channel-types">
767 <div class="empty-state" style="padding:12px;font-size:12px">no channel types configured</div>
768 </div>
769
770 <div id="topo-save-result" style="display:none;margin-top:12px"></div>
771 </div>
772 </div>
773
774 <!-- about -->
775 <div class="card">
776 <div class="card-header" style="cursor:default"><h2>about</h2></div>
777 <div class="card-body" style="font-size:13px;color:#8b949e;line-height:1.8">
@@ -629,11 +793,11 @@
793 <div class="pane-inner">
794
795 <!-- LLM backends -->
796 <div class="card" id="card-ai-backends">
797 <div class="card-header" style="cursor:default">
798 <h2>LLM backends</h2><span class="card-desc">configured providers for oracle and other LLM bots</span>
799 <div class="spacer"></div>
800 <button class="sm" onclick="loadAI()">↺ refresh</button>
801 <button class="sm primary" onclick="openAddBackend()">+ add backend</button>
802 </div>
803 <div class="card-body" style="padding:0">
@@ -768,19 +932,19 @@
932 </div>
933 </div>
934
935 <!-- supported backends reference -->
936 <div class="card" id="card-ai-supported">
937 <div class="card-header" onclick="toggleCard('card-ai-supported',event)"><h2>supported backends</h2><span class="card-desc">all available provider types</span><span class="collapse-icon">▾</span></div>
938 <div class="card-body" id="ai-supported-list">
939 <div class="empty-state">loading…</div>
940 </div>
941 </div>
942
943 <!-- config example -->
944 <div class="card" id="card-ai-example" style="display:none">
945 <div class="card-header" onclick="toggleCard('card-ai-example',event)"><h2>YAML example</h2><span class="card-desc">copy-paste starter config</span><span class="collapse-icon">▾</span></div>
946 <div class="card-body">
947 <pre style="font-size:12px;color:#a5d6ff;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:12px;overflow-x:auto;white-space:pre">llm:
948 backends:
949 - name: openai-main
950 backend: openai
@@ -2321,10 +2485,11 @@
2485 renderBehaviors(s.policies.behaviors || []);
2486 renderAgentPolicy(s.policies.agent_policy || {});
2487 renderBridgePolicy(s.policies.bridge || {});
2488 renderLoggingPolicy(s.policies.logging || {});
2489 loadAdmins();
2490 loadConfigCards();
2491 } catch(e) {
2492 document.getElementById('tls-badge').textContent = 'error';
2493 }
2494 }
2495
@@ -2437,42 +2602,295 @@
2602 const rot = document.getElementById('policy-log-rotation').value;
2603 document.getElementById('policy-log-size-row').style.display = rot === 'size' ? '' : 'none';
2604 }
2605
2606 async function savePolicies() {
2607 // Saves behaviors only — agent_policy, logging, and bridge are now
2608 // persisted to scuttlebot.yaml via PUT /v1/config.
2609 if (!currentPolicies) return;
2610 const p = JSON.parse(JSON.stringify(currentPolicies)); // deep copy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2611 const resultEl = document.getElementById('policies-save-result');
2612 try {
2613 currentPolicies = await api('PUT', '/v1/settings/policies', p);
2614 resultEl.style.display = 'block';
2615 resultEl.innerHTML = renderAlert('success', 'Behaviors saved.');
2616 setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
2617 } catch(e) {
2618 resultEl.style.display = 'block';
2619 resultEl.innerHTML = renderAlert('error', e.message);
2620 }
2621 }
2622
2623 // --- topology config ---
2624 let _topoChannels = [];
2625 let _topoTypes = [];
2626
2627 function renderTopoStaticChannels() {
2628 const el = document.getElementById('topo-static-channels');
2629 if (!_topoChannels.length) {
2630 el.innerHTML = '<div class="empty-state" style="padding:12px;font-size:12px">no static channels configured</div>';
2631 return;
2632 }
2633 el.innerHTML = `
2634 <table style="width:100%;font-size:12px;border-collapse:collapse">
2635 <thead>
2636 <tr style="color:#8b949e;border-bottom:1px solid #30363d">
2637 <th style="text-align:left;padding:4px 8px;font-weight:500">name</th>
2638 <th style="text-align:left;padding:4px 8px;font-weight:500">topic</th>
2639 <th style="text-align:left;padding:4px 8px;font-weight:500">autojoin</th>
2640 <th style="padding:4px 8px"></th>
2641 </tr>
2642 </thead>
2643 <tbody>
2644 ${_topoChannels.map((c, i) => `
2645 <tr style="border-bottom:1px solid #21262d">
2646 <td style="padding:4px 8px">
2647 <input type="text" value="${esc(c.name||'')}" style="width:120px;padding:2px 6px;font-size:11px"
2648 onchange="_topoChannels[${i}].name=this.value">
2649 </td>
2650 <td style="padding:4px 8px">
2651 <input type="text" value="${esc(c.topic||'')}" style="width:160px;padding:2px 6px;font-size:11px"
2652 onchange="_topoChannels[${i}].topic=this.value">
2653 </td>
2654 <td style="padding:4px 8px">
2655 <input type="text" value="${esc((c.autojoin||[]).join(', '))}" placeholder="bridge, sentinel"
2656 style="width:160px;padding:2px 6px;font-size:11px"
2657 onchange="_topoChannels[${i}].autojoin=this.value.split(',').map(s=>s.trim()).filter(Boolean)">
2658 </td>
2659 <td style="padding:4px 8px;text-align:right">
2660 <button class="sm" onclick="topoDeleteStaticChannel(${i})">✕</button>
2661 </td>
2662 </tr>
2663 `).join('')}
2664 </tbody>
2665 </table>`;
2666 }
2667
2668 function renderTopoChannelTypes() {
2669 const el = document.getElementById('topo-channel-types');
2670 if (!_topoTypes.length) {
2671 el.innerHTML = '<div class="empty-state" style="padding:12px;font-size:12px">no channel types configured</div>';
2672 return;
2673 }
2674 el.innerHTML = `
2675 <table style="width:100%;font-size:12px;border-collapse:collapse">
2676 <thead>
2677 <tr style="color:#8b949e;border-bottom:1px solid #30363d">
2678 <th style="text-align:left;padding:4px 8px;font-weight:500">name</th>
2679 <th style="text-align:left;padding:4px 8px;font-weight:500">prefix</th>
2680 <th style="text-align:left;padding:4px 8px;font-weight:500">ttl</th>
2681 <th style="text-align:left;padding:4px 8px;font-weight:500">ephemeral</th>
2682 <th style="padding:4px 8px"></th>
2683 </tr>
2684 </thead>
2685 <tbody>
2686 ${_topoTypes.map((x, i) => `
2687 <tr style="border-bottom:1px solid #21262d">
2688 <td style="padding:4px 8px">
2689 <input type="text" value="${esc(x.name||'')}" style="width:100px;padding:2px 6px;font-size:11px"
2690 onchange="_topoTypes[${i}].name=this.value">
2691 </td>
2692 <td style="padding:4px 8px">
2693 <input type="text" value="${esc(x.prefix||'')}" placeholder="task." style="width:100px;padding:2px 6px;font-size:11px"
2694 onchange="_topoTypes[${i}].prefix=this.value">
2695 </td>
2696 <td style="padding:4px 8px">
2697 <input type="text" value="${esc(x.ttl||'')}" placeholder="24h" style="width:80px;padding:2px 6px;font-size:11px"
2698 title="Duration string e.g. 1h, 24h, 72h"
2699 onchange="_topoTypes[${i}].ttl=this.value">
2700 </td>
2701 <td style="padding:4px 8px;text-align:center">
2702 <input type="checkbox" ${x.ephemeral ? 'checked' : ''} style="width:auto"
2703 onchange="_topoTypes[${i}].ephemeral=this.checked">
2704 </td>
2705 <td style="padding:4px 8px;text-align:right">
2706 <button class="sm" onclick="topoDeleteChannelType(${i})">✕</button>
2707 </td>
2708 </tr>
2709 `).join('')}
2710 </tbody>
2711 </table>`;
2712 }
2713
2714 function topoAddStaticChannel() {
2715 _topoChannels.push({name: '', topic: '', autojoin: []});
2716 renderTopoStaticChannels();
2717 // focus the last name input
2718 const rows = document.querySelectorAll('#topo-static-channels tbody tr');
2719 if (rows.length) rows[rows.length-1].querySelector('input').focus();
2720 }
2721
2722 function topoAddChannelType() {
2723 _topoTypes.push({name: '', prefix: '', ephemeral: false, ttl: ''});
2724 renderTopoChannelTypes();
2725 const rows = document.querySelectorAll('#topo-channel-types tbody tr');
2726 if (rows.length) rows[rows.length-1].querySelector('input').focus();
2727 }
2728
2729 function topoDeleteStaticChannel(idx) {
2730 _topoChannels.splice(idx, 1);
2731 renderTopoStaticChannels();
2732 }
2733
2734 function topoDeleteChannelType(idx) {
2735 _topoTypes.splice(idx, 1);
2736 renderTopoChannelTypes();
2737 }
2738
2739 async function saveTopologyConfig() {
2740 const resultEl = document.getElementById('topo-save-result');
2741 const channels = _topoChannels.filter(c => c.name.trim());
2742 const types = _topoTypes.filter(x => x.name.trim() && x.prefix.trim());
2743 const payload = {
2744 topology: {
2745 nick: document.getElementById('topo-nick').value.trim() || 'topology',
2746 channels: channels,
2747 types: types,
2748 },
2749 config_history: {
2750 keep: parseInt(document.getElementById('topo-history-keep').value, 10) || 20,
2751 },
2752 };
2753 try {
2754 const res = await api('PUT', '/v1/config', payload);
2755 resultEl.style.display = 'block';
2756 let msg = 'Topology config saved.';
2757 if (res.restart_required && res.restart_required.length) {
2758 msg += ' Restart required for: ' + res.restart_required.join(', ') + '.';
2759 }
2760 resultEl.innerHTML = renderAlert('success', msg);
2761 setTimeout(() => { resultEl.style.display = 'none'; }, 4000);
2762 } catch(e) {
2763 resultEl.style.display = 'block';
2764 resultEl.innerHTML = renderAlert('error', e.message);
2765 }
2766 }
2767
2768 // --- shared config save helper ---
2769 async function saveConfigPatch(patch, resultElId) {
2770 const resultEl = document.getElementById(resultElId);
2771 try {
2772 const res = await api('PUT', '/v1/config', patch);
2773 let msg = 'Saved.';
2774 if (res.restart_required && res.restart_required.length) {
2775 msg += ' Restart required for: ' + res.restart_required.join(', ') + '.';
2776 }
2777 resultEl.style.display = 'block';
2778 resultEl.innerHTML = renderAlert('success', msg);
2779 setTimeout(() => { resultEl.style.display = 'none'; }, 4000);
2780 } catch(e) {
2781 resultEl.style.display = 'block';
2782 resultEl.innerHTML = renderAlert('error', e.message);
2783 }
2784 }
2785
2786 // --- config-backed cards ---
2787 async function loadConfigCards() {
2788 try {
2789 const cfg = await api('GET', '/v1/config');
2790 // general
2791 document.getElementById('general-api-addr').value = cfg.api_addr || '';
2792 document.getElementById('general-mcp-addr').value = cfg.mcp_addr || '';
2793 // ergo
2794 const e = cfg.ergo || {};
2795 document.getElementById('ergo-network-name').value = e.network_name || '';
2796 document.getElementById('ergo-server-name').value = e.server_name || '';
2797 document.getElementById('ergo-irc-addr').value = e.irc_addr || '';
2798 document.getElementById('ergo-external').checked = !!e.external;
2799 // tls
2800 const t = cfg.tls || {};
2801 document.getElementById('tls-domain').value = t.domain || '';
2802 document.getElementById('tls-email').value = t.email || '';
2803 document.getElementById('tls-allow-insecure').checked = !!t.allow_insecure;
2804 // bridge (full)
2805 const b = cfg.bridge || {};
2806 document.getElementById('bridge-enabled').checked = b.enabled !== false;
2807 document.getElementById('bridge-nick').value = b.nick || '';
2808 document.getElementById('bridge-channels').value = (b.channels || []).join(', ');
2809 document.getElementById('bridge-buffer-size').value = b.buffer_size || '';
2810 document.getElementById('policy-bridge-web-user-ttl').value = b.web_user_ttl_minutes || 5;
2811 // topology + history
2812 const topo = cfg.topology || {};
2813 const h = cfg.config_history || {};
2814 _topoChannels = (topo.channels || []).map(c => Object.assign({}, c));
2815 _topoTypes = (topo.types || []).map(x => Object.assign({}, x));
2816 document.getElementById('topo-nick').value = topo.nick || 'topology';
2817 document.getElementById('topo-history-keep').value = h.keep != null ? h.keep : 20;
2818 renderTopoStaticChannels();
2819 renderTopoChannelTypes();
2820 } catch(e) {
2821 console.error('loadConfigCards:', e);
2822 }
2823 }
2824
2825 function saveAgentPolicy() {
2826 saveConfigPatch({
2827 agent_policy: {
2828 require_checkin: document.getElementById('policy-checkin-enabled').checked,
2829 checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2830 required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2831 }
2832 }, 'agentpolicy-save-result');
2833 }
2834
2835 function saveBridgeConfig() {
2836 saveConfigPatch({
2837 bridge: {
2838 enabled: document.getElementById('bridge-enabled').checked,
2839 nick: document.getElementById('bridge-nick').value.trim() || undefined,
2840 channels: document.getElementById('bridge-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2841 buffer_size: parseInt(document.getElementById('bridge-buffer-size').value, 10) || undefined,
2842 web_user_ttl_minutes: parseInt(document.getElementById('policy-bridge-web-user-ttl').value, 10) || 5,
2843 }
2844 }, 'bridge-save-result');
2845 }
2846
2847 function saveLogging() {
2848 saveConfigPatch({
2849 logging: {
2850 enabled: document.getElementById('policy-logging-enabled').checked,
2851 dir: document.getElementById('policy-log-dir').value.trim(),
2852 format: document.getElementById('policy-log-format').value,
2853 rotation: document.getElementById('policy-log-rotation').value,
2854 max_size_mb: parseInt(document.getElementById('policy-log-max-size').value, 10) || 0,
2855 per_channel: document.getElementById('policy-log-per-channel').checked,
2856 max_age_days: parseInt(document.getElementById('policy-log-max-age').value, 10) || 0,
2857 }
2858 }, 'logging-save-result');
2859 }
2860
2861 function saveGeneralConfig() {
2862 const patch = {};
2863 const addr = document.getElementById('general-api-addr').value.trim();
2864 const mcp = document.getElementById('general-mcp-addr').value.trim();
2865 if (addr) patch.api_addr = addr;
2866 if (mcp) patch.mcp_addr = mcp;
2867 saveConfigPatch(patch, 'general-save-result');
2868 }
2869
2870 function saveErgoConfig() {
2871 saveConfigPatch({
2872 ergo: {
2873 network_name: document.getElementById('ergo-network-name').value.trim() || undefined,
2874 server_name: document.getElementById('ergo-server-name').value.trim() || undefined,
2875 irc_addr: document.getElementById('ergo-irc-addr').value.trim() || undefined,
2876 external: document.getElementById('ergo-external').checked,
2877 }
2878 }, 'ergo-save-result');
2879 }
2880
2881 function saveTLSConfig() {
2882 saveConfigPatch({
2883 tls: {
2884 domain: document.getElementById('tls-domain').value.trim() || undefined,
2885 email: document.getElementById('tls-email').value.trim() || undefined,
2886 allow_insecure: document.getElementById('tls-allow-insecure').checked,
2887 }
2888 }, 'tls-config-save-result');
2889 }
2890
2891 // --- init ---
2892 function loadAll() { loadStatus(); loadAgents(); loadSettings(); startMetricsPoll(); }
2893 initAuth();
2894 </script>
2895 </body>
2896 </html>
2897

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button