This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/pages/templates/surfaces/admin.html
2026-03-14 16:46:16 +00:00

272 lines
16 KiB
HTML

{{/*
Admin surface — matches switchboard-prototype-admin.jsx.
Full-page layout: topbar (with category tabs) + left nav + content area.
JS (admin-handlers.js, ui-admin.js) populates dynamic sections.
*/}}
{{define "surface-admin"}}
<div class="surface-admin">
{{/* Top Bar */}}
<div class="admin-topbar">
<a href="{{.BasePath}}/" class="admin-topbar-back" id="adminBackBtn">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
Back
</a>
<div class="admin-topbar-sep"></div>
<span class="admin-topbar-title"><img src="{{.BasePath}}/favicon.svg?v={{.Version}}" alt="" width="18" height="18" style="vertical-align: -3px;"> Administration</span>
{{/* Category tabs */}}
<div class="admin-category-tabs" id="adminCategoryTabs">
{{$section := .Section}}
{{$base := .BasePath}}
<a href="{{$base}}/admin/users" class="admin-cat-btn{{if eq $section "users"}} active{{else if eq $section "teams"}} active{{else if eq $section "groups"}} active{{end}}" data-cat="people">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
People
</a>
<a href="{{$base}}/admin/providers" class="admin-cat-btn{{if eq $section "providers"}} active{{else if eq $section "models"}} active{{else if eq $section "personas"}} active{{else if eq $section "roles"}} active{{else if eq $section "knowledgeBases"}} active{{else if eq $section "memory"}} active{{end}}" data-cat="ai">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/></svg>
AI
</a>
<a href="{{$base}}/admin/workflows" class="admin-cat-btn{{if eq $section "workflows"}} active{{end}}" data-cat="workflows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
Workflows
</a>
<a href="{{$base}}/admin/health" class="admin-cat-btn{{if eq $section "health"}} active{{else if eq $section "routing"}} active{{else if eq $section "capabilities"}} active{{end}}" data-cat="routing">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
Routing
</a>
<a href="{{$base}}/admin/settings" class="admin-cat-btn{{if eq $section "settings"}} active{{else if eq $section "storage"}} active{{else if eq $section "extensions"}} active{{else if eq $section "channels"}} active{{else if eq $section "surfaces"}} active{{end}}" data-cat="system">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09"/></svg>
System
</a>
<a href="{{$base}}/admin/usage" class="admin-cat-btn{{if eq $section "usage"}} active{{else if eq $section "audit"}} active{{else if eq $section "stats"}} active{{end}}" data-cat="monitoring">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
Monitoring
</a>
</div>
</div>
<div class="admin-body">
{{/* Left Nav — populated by admin-scaffold.js from ADMIN_SECTIONS */}}
<div class="admin-nav" id="adminNav"></div>
{{/* Content Area */}}
<div class="admin-content">
<div class="admin-content-header">
<h2 id="adminSectionTitle">{{$section}}</h2>
<div style="flex:1;"></div>
<div class="admin-search">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-3);flex-shrink:0;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input id="adminSearch" placeholder="Search…" autocomplete="off">
</div>
<button id="adminAddBtn" class="btn-md btn-primary" style="display:none;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add
</button>
</div>
<div class="admin-content-body" id="adminContentBody" data-section="{{$section}}">
{{/* Populated by admin-handlers.js */}}
<div class="settings-placeholder" id="adminDynamic">Loading…</div>
</div>
</div>
</div>
</div>
{{/* Create User modal */}}
<div id="createUserModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Create User</h2>
<button class="modal-close" onclick="sb.call('closeModal','createUserModal')">&#10005;</button>
</div>
<div class="modal-body">
<div class="form-group"><label>Username</label><input type="text" id="adminNewUsername" placeholder="username" autocomplete="off"></div>
<div class="form-group"><label>Email</label><input type="text" id="adminNewEmail" placeholder="email@example.com" autocomplete="off"></div>
<div class="form-group"><label>Password</label><input type="password" id="adminNewPassword" placeholder="min 8 chars" autocomplete="new-password"></div>
<div class="form-group"><label>Role</label><select id="adminNewRole"><option value="user">User</option><option value="admin">Admin</option></select></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','createUserModal')">Cancel</button>
<button class="btn-md btn-primary" id="adminCreateUserBtn">Create User</button>
</div>
</div>
</div>
{{/* Reset Password modal */}}
<div id="resetPwModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Reset Password</h2>
<button class="modal-close" onclick="sb.call('closeModal','resetPwModal')">&#10005;</button>
</div>
<div class="modal-body">
<p id="resetPwUser" style="font-weight:600;margin-bottom:8px;"></p>
<div class="admin-warning" style="background:var(--danger-dim);border:1px solid var(--danger);border-radius:var(--radius);padding:10px 12px;font-size:12px;margin-bottom:12px;color:var(--danger);">
<strong>Warning:</strong> This will destroy the user's personal vault. All personal API keys (BYOK) will be permanently deleted.
</div>
<div class="form-group"><label>New Password</label><input type="password" id="resetPwInput" placeholder="min 8 chars" autocomplete="new-password"></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','resetPwModal')">Cancel</button>
<button class="btn-md btn-danger" id="resetPwConfirmBtn">Reset Password & Destroy Vault</button>
</div>
</div>
</div>
{{/* Approve User modal */}}
<div id="approveUserModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2 id="approveUserTitle">Approve User</h2>
<button class="modal-close" onclick="sb.call('closeModal','approveUserModal')">&#10005;</button>
</div>
<div class="modal-body">
<input type="hidden" id="approveUserId">
<div id="approveTeamsList"></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','approveUserModal')">Cancel</button>
<button class="btn-md btn-primary" id="approveUserSubmitBtn">Activate User</button>
</div>
</div>
</div>
{{/* Create Team modal */}}
<div id="createTeamModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Create Team</h2>
<button class="modal-close" onclick="sb.call('closeModal','createTeamModal')">&#10005;</button>
</div>
<div class="modal-body">
<div class="form-group"><label>Name</label><input type="text" id="adminTeamName" placeholder="Team name"></div>
<div class="form-group"><label>Description</label><input type="text" id="adminTeamDesc" placeholder="Optional description"></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','createTeamModal')">Cancel</button>
<button class="btn-md btn-primary" id="adminCreateTeamBtn">Create Team</button>
</div>
</div>
</div>
{{/* Create Group modal */}}
<div id="createGroupModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Create Group</h2>
<button class="modal-close" onclick="sb.call('closeModal','createGroupModal')">&#10005;</button>
</div>
<div class="modal-body">
<div class="form-group"><label>Name</label><input type="text" id="adminGroupName" placeholder="Group name"></div>
<div class="form-group"><label>Description</label><input type="text" id="adminGroupDesc" placeholder="Optional description"></div>
<div style="display:flex;gap:12px;">
<div class="form-group" style="flex:1"><label>Scope</label><select id="adminGroupScope"><option value="global">Global</option><option value="team">Team</option></select></div>
<div class="form-group" id="adminGroupTeamRow" style="flex:1;display:none"><label>Team</label><select id="adminGroupTeam"></select></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','createGroupModal')">Cancel</button>
<button class="btn-md btn-primary" id="adminCreateGroupBtn">Create Group</button>
</div>
</div>
</div>
{{/* Provider form modal */}}
<div id="providerFormModal" class="modal-overlay">
<div class="modal" style="max-width:520px;">
<div class="modal-header">
<h2 id="providerFormTitle">Add Provider</h2>
<button class="modal-close" onclick="sb.call('closeModal','providerFormModal')">&#10005;</button>
</div>
<div class="modal-body" id="adminAddProviderForm"></div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','providerFormModal')">Cancel</button>
</div>
</div>
</div>
{{/* Persona form modal */}}
<div id="personaFormModal" class="modal-overlay">
<div class="modal" style="max-width:520px;">
<div class="modal-header">
<h2 id="personaFormTitle">Create Persona</h2>
<button class="modal-close" onclick="sb.call('closeModal','personaFormModal')">&#10005;</button>
</div>
<div class="modal-body" id="adminAddPersonaForm"></div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','personaFormModal')">Cancel</button>
</div>
</div>
</div>
{{end}}
{{/* CSS: loaded via base.html (variables, layout, primitives, modals, chat, panels, surfaces, splash) */}}
{{define "css-admin"}}{{end}}
{{/* Scripts */}}
{{define "scripts-admin"}}
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-admin.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-handlers.js?v={{.Version}}"></script>
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/persona-kb.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/memory-ui.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/notifications.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/files.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-scaffold.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/workflow-api.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/workflow-admin.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/task-admin.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-surfaces.js?v={{.Version}}"></script>
<script nonce="{{.CSPNonce}}">
document.addEventListener('DOMContentLoaded', () => {
// ── Admin navigation: don't pollute browser history ──
// Same pattern as settings surface: stash return URL on entry,
// tab/nav switches use location.replace(), back goes directly out.
const RETURN_KEY = 'sb_admin_return';
if (!sessionStorage.getItem(RETURN_KEY)) {
const ref = document.referrer;
const base = (window.__BASE__ || '') + '/';
if (ref && ref.startsWith(location.origin) && !ref.includes('/admin')) {
sessionStorage.setItem(RETURN_KEY, ref);
} else {
sessionStorage.setItem(RETURN_KEY, location.origin + base);
}
}
const backBtn = document.getElementById('adminBackBtn');
if (backBtn) {
backBtn.addEventListener('click', (e) => {
e.preventDefault();
const returnURL = sessionStorage.getItem(RETURN_KEY);
sessionStorage.removeItem(RETURN_KEY);
location.href = returnURL || (window.__BASE__ || '') + '/';
});
}
// Category tabs: replace history instead of pushing
document.querySelectorAll('.admin-cat-btn').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
location.replace(link.href);
});
});
// Nav links are dynamically rebuilt by admin-scaffold.js —
// use event delegation on the nav container
const adminNav = document.getElementById('adminNav');
if (adminNav) {
adminNav.addEventListener('click', (e) => {
const link = e.target.closest('.admin-nav-link');
if (link && link.href) {
e.preventDefault();
location.replace(link.href);
}
});
}
});
</script>
{{end}}