Fix agent creation UI: scope selector + agent widget on main screen

1. Fixed 'Scoped' agent creation:
   - Scope selector now appears when 'Scoped' is selected
   - Loads available scopes from entries
   - Shows checkboxes for each scope
   - Select All/None button
   - Validates at least one scope selected

2. Fixed token display:
   - Uses result.credential (was result.token)
   - Uses result.scopes (was result.scope)

3. Added agent widget to main screen:
   - Shows active agent count
   - Click to open agent management
   - Only appears when agents exist

4. Removed Admin option from create/edit (agents can't manage agents)
This commit is contained in:
James 2026-04-02 02:42:35 -04:00
parent d3e9f89bc0
commit 2410a2d128
1 changed files with 99 additions and 7 deletions

View File

@ -499,6 +499,7 @@
'<button onclick="showNewEntry()" class="btn btn-primary btn-sm">+ New</button>' +
'</div></div>' +
'<div id="listStats" class="list-stats"></div>' +
'<div id="agentWidget" class="agent-widget hidden"></div>' +
'<div class="main-content">' +
'<div id="listPane" class="split-list"></div>' +
'<div id="detailPane" class="split-detail"><div class="split-detail-empty">Select an entry</div></div>' +
@ -542,6 +543,7 @@
entries = await api('GET', '/api/entries');
history.replaceState({list: true}, '', '/app/');
renderEntryList();
loadAgentWidget(); // Load agents count
if (autoImport && entries.length === 0) {
location.href = '/app/import.html';
}
@ -554,6 +556,26 @@
}
}
async function loadAgentWidget() {
try {
var agents = await api('GET', '/api/agents');
var widget = document.getElementById('agentWidget');
if (!agents || agents.length === 0) {
widget.classList.add('hidden');
return;
}
var active = agents.filter(function(a) { return a.status === 'active'; }).length;
widget.innerHTML = '<div onclick="showAgents()" style="cursor:pointer;display:flex;align-items:center;gap:0.5rem;padding:0.5rem 1rem;background:var(--surface-1);border-bottom:1px solid var(--border)">' +
'<span style="font-size:1.2rem">🤖</span>' +
'<span>' + active + ' active agent' + (active !== 1 ? 's' : '') + '</span>' +
'<span style="margin-left:auto;color:var(--muted);font-size:0.75rem">Click to manage</span>' +
'</div>';
widget.classList.remove('hidden');
} catch(e) {
// Silently fail - agents not critical
}
}
function domainFrom(url) {
try { return new URL(url).hostname.replace(/^www\./, ''); } catch(e) { return url; }
}
@ -1311,18 +1333,78 @@
'</div>' +
'<div class="form-group mb-3">' +
'<label class="form-label">Access level</label>' +
'<select id="agentAccess" class="form-input">' +
'<select id="agentAccess" class="form-input" onchange="toggleScopeSelector()">' +
'<option value="scoped">Scoped (only assigned entries)</option>' +
'<option value="all">Full read access (all entries)</option>' +
'<option value="admin">Admin (full access + manage agents)</option>' +
'</select>' +
'</div>' +
'<div id="scopeSelector" class="form-group mb-3" style="display:none">' +
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem">' +
'<label class="form-label" style="margin:0">Select scopes</label>' +
'<button onclick="toggleAllScopes()" class="btn btn-ghost btn-sm">Select All / None</button>' +
'</div>' +
'<div id="scopeList" style="max-height:200px;overflow-y:auto;border:1px solid var(--border);border-radius:0.5rem;padding:0.75rem;background:var(--surface-1)">' +
'<p class="text-muted">Loading scopes...</p>' +
'</div>' +
'</div>' +
'<div class="modal-actions">' +
'<button onclick="doCreateAgent()" class="btn btn-primary">Create</button>' +
'<button onclick="showAgents()" class="btn btn-ghost">Back</button>' +
'</div>' +
'</div>';
document.getElementById('agentName').focus();
loadAvailableScopes();
toggleScopeSelector(); // Show/hide based on initial selection
}
var availableScopes = [];
async function loadAvailableScopes() {
try {
var entries = await api('GET', '/api/entries');
var scopeSet = new Set();
entries.forEach(function(e) {
if (e.scopes && e.scopes !== '0000') {
e.scopes.split(',').forEach(function(s) {
if (s.trim()) scopeSet.add(s.trim());
});
}
});
availableScopes = Array.from(scopeSet).sort();
renderScopeList();
} catch(e) {
document.getElementById('scopeList').innerHTML = '<p class="text-red">Failed to load scopes</p>';
}
}
function renderScopeList() {
var container = document.getElementById('scopeList');
if (availableScopes.length === 0) {
container.innerHTML = '<p class="text-muted">No custom scopes found. Create entries with scopes first.</p>';
return;
}
var html = '';
availableScopes.forEach(function(scope) {
html += '<label style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;cursor:pointer">' +
'<input type="checkbox" name="agentScope" value="' + scope + '" style="accent-color:var(--text)">' +
'<code style="font-size:0.8rem">' + scope + '</code>' +
'</label>';
});
container.innerHTML = html;
}
function toggleScopeSelector() {
var access = document.getElementById('agentAccess').value;
var selector = document.getElementById('scopeSelector');
selector.style.display = access === 'scoped' ? 'block' : 'none';
}
function toggleAllScopes() {
var checkboxes = document.querySelectorAll('input[name="agentScope"]');
var allChecked = Array.from(checkboxes).every(function(cb) { return cb.checked; });
checkboxes.forEach(function(cb) {
cb.checked = !allChecked;
});
}
async function doCreateAgent() {
@ -1330,11 +1412,22 @@
if (!name) { toast('Name is required', 'error'); return; }
var access = document.getElementById('agentAccess').value;
var scopes = '0000'; // Default scope
if (access === 'scoped') {
var selected = document.querySelectorAll('input[name="agentScope"]:checked');
if (selected.length === 0) {
toast('Select at least one scope, or choose "Full read access"', 'error');
return;
}
scopes = Array.from(selected).map(function(cb) { return cb.value; }).join(',');
}
try {
var result = await api('POST', '/api/agents', {
name: name,
all_access: access === 'all' || access === 'admin',
admin: access === 'admin'
scopes: scopes,
all_access: access === 'all',
admin: false
});
showAgentToken(result);
} catch (err) {
@ -1346,10 +1439,10 @@
var content = document.getElementById('modalContent');
content.innerHTML = '<div class="modal-body">' +
'<h3 class="modal-title">Agent Created</h3>' +
'<p class="mb-2"><strong>' + escapeHtml(result.name) + '</strong> — scope <code>' + escapeHtml(result.scope) + '</code></p>' +
'<p class="mb-2"><strong>' + escapeHtml(result.name) + '</strong> — scopes: <code>' + escapeHtml(result.scopes) + '</code></p>' +
'<p class="text-subtle mb-3">Copy this token now. It will never be shown again.</p>' +
'<div class="token-display" style="background:var(--surface-1);border:1px solid var(--border);border-radius:0.5rem;padding:1rem;font-family:JetBrains Mono,monospace;font-size:0.8rem;word-break:break-all;user-select:all;cursor:pointer" onclick="navigator.clipboard.writeText(this.textContent);toast(\'Token copied\')">' +
escapeHtml(result.token) +
escapeHtml(result.credential) +
'</div>' +
'<p class="text-subtle mt-2" style="font-size:0.75rem">Click to copy. Use this token as the Bearer in CLI: <code>clavitor-cli init &lt;token&gt;</code></p>' +
'<div class="modal-actions mt-4">' +
@ -1377,7 +1470,6 @@
'<select id="editAgentAccess" class="form-input">' +
'<option value="scoped"' + (!agent.all_access ? ' selected' : '') + '>Scoped</option>' +
'<option value="all"' + (agent.all_access && !agent.admin ? ' selected' : '') + '>Full read access</option>' +
'<option value="admin"' + (agent.admin ? ' selected' : '') + '>Admin</option>' +
'</select>' +
'</div>' +
'<div class="modal-actions">' +