mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
feat: admin panel — right-aligned sidebar navigation with two-column … (#52)
* feat: admin panel — right-aligned sidebar navigation with two-column modals
Replace the horizontal tab bar (11 tabs, overflowing on standard monitors)
with a grouped sidebar on the right side, matching the admin button's
position in the header for natural spatial flow.
Sidebar: 5 groups (Identity, Automation, Governance, Observe, System) with
12 nav items including new Settings stub. Always visible on desktop (180px),
off-canvas drawer on mobile (<700px) sliding from right with backdrop.
Admin button: toggle behavior (click again to return to overview), active
state with amber highlight + top accent line, aria-expanded management.
Breadcrumb: shows active tab ("Admin / Users", "Admin / Audit", etc).
Modals: WS Template and Schedule create/edit forms restructured into
two-column grid (820px) with "Identity"/"Model Config" and
"Schedule"/"Execution" column headings. All modals gain max-height: 85vh
+ overflow-y: auto safety net. Modal z-index bumped to 600 (above sidebar).
Also: "Tokens" renamed to "API Tokens", redundant "Server default"
placeholders removed from model config fields, view fade-in transition,
comprehensive ARIA (grouped sidebar, aria-hidden on mobile, focus return
on drawer close), reduced-motion support.
* fix: address Copilot review — aria-orientation, settings permission gate, inert sidebar
- Add aria-orientation="vertical" to sidebar tablist for assistive tech
- Gate Settings tab behind admin.users permission so empty-state logic
works correctly when user has no admin permissions
- Use inert attribute on mobile sidebar when closed to prevent keyboard
focus from reaching off-canvas controls
- Add resize listener to sync aria-hidden/inert when crossing the
700px mobile breakpoint
This commit is contained in:
@@ -13,13 +13,25 @@ var _cfTrapHandler = null;
|
||||
var _adminWatches = [];
|
||||
var _confirmCallbackFn = null;
|
||||
var _confirmTriggerEl = null;
|
||||
var _mobileSidebarOpen = false;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// View switching (called from app.js showOverview/drillDown pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function showAdmin() {
|
||||
/* global currentView */
|
||||
/* global currentView, showOverview */
|
||||
// Toggle: if already in admin view, go back to overview
|
||||
if (currentView === "admin") {
|
||||
var adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.remove("active");
|
||||
adminBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
showOverview();
|
||||
return;
|
||||
}
|
||||
|
||||
currentView = "admin";
|
||||
document.getElementById("view-overview").style.display = "none";
|
||||
document.getElementById("view-node").style.display = "none";
|
||||
@@ -28,9 +40,16 @@ function showAdmin() {
|
||||
document.getElementById("breadcrumb").style.display = "";
|
||||
document.getElementById("breadcrumb-label").textContent = "Admin";
|
||||
document.getElementById("main").scrollTop = 0;
|
||||
|
||||
// Highlight admin button as active
|
||||
var adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.add("active");
|
||||
adminBtn.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
history.pushState({ view: "admin" }, "");
|
||||
|
||||
// Permission gating: hide tabs the user cannot access
|
||||
// Permission gating: hide nav items the user cannot access
|
||||
var perms = sessionStorage.getItem("turnstone_permissions") || "";
|
||||
var tabPerms = {
|
||||
users: "admin.users",
|
||||
@@ -44,29 +63,65 @@ function showAdmin() {
|
||||
"ws-templates": "admin.ws_templates",
|
||||
usage: "admin.usage",
|
||||
audit: "admin.audit",
|
||||
settings: "admin.users",
|
||||
};
|
||||
if (perms) {
|
||||
var permSet = perms.split(",");
|
||||
var tabs = document.querySelectorAll(".admin-tab");
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
var tabName = tabs[i].getAttribute("data-tab");
|
||||
var navItems = document.querySelectorAll(".admin-nav");
|
||||
for (var i = 0; i < navItems.length; i++) {
|
||||
var tabName = navItems[i].getAttribute("data-tab");
|
||||
var needed = tabPerms[tabName];
|
||||
if (needed && permSet.indexOf(needed) < 0) {
|
||||
tabs[i].style.display = "none";
|
||||
navItems[i].style.display = "none";
|
||||
} else {
|
||||
tabs[i].style.display = "";
|
||||
navItems[i].style.display = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to the first visible tab
|
||||
var visibleTabs = document.querySelectorAll(
|
||||
'.admin-tab:not([style*="display: none"])',
|
||||
);
|
||||
if (visibleTabs.length > 0) {
|
||||
switchAdminTab(visibleTabs[0].getAttribute("data-tab"));
|
||||
// Hide groups where all children are permission-hidden
|
||||
var groups = document.querySelectorAll(".admin-sidebar-group");
|
||||
for (var g = 0; g < groups.length; g++) {
|
||||
var visibleInGroup = groups[g].querySelectorAll(
|
||||
'.admin-nav:not([style*="display: none"])',
|
||||
);
|
||||
groups[g].style.display = visibleInGroup.length > 0 ? "" : "none";
|
||||
}
|
||||
|
||||
// Mobile: ensure sidebar starts hidden + inert; desktop: ensure it's accessible
|
||||
var sidebar = document.getElementById("admin-sidebar");
|
||||
if (window.innerWidth <= 700) {
|
||||
_mobileSidebarOpen = false;
|
||||
sidebar.classList.add("collapsed");
|
||||
sidebar.classList.remove("open");
|
||||
sidebar.setAttribute("aria-hidden", "true");
|
||||
sidebar.setAttribute("inert", "");
|
||||
} else {
|
||||
// No tabs visible — show empty state instead of loading an inaccessible tab
|
||||
sidebar.removeAttribute("aria-hidden");
|
||||
sidebar.removeAttribute("inert");
|
||||
}
|
||||
|
||||
// Mobile backdrop listener (idempotent)
|
||||
var backdrop = document.getElementById("admin-sidebar-backdrop");
|
||||
if (backdrop && !backdrop._listenerAttached) {
|
||||
backdrop.addEventListener("click", function () {
|
||||
if (_mobileSidebarOpen) {
|
||||
_toggleMobileSidebar();
|
||||
var mt = document.getElementById("admin-mobile-toggle");
|
||||
if (mt) mt.focus();
|
||||
}
|
||||
});
|
||||
backdrop._listenerAttached = true;
|
||||
}
|
||||
|
||||
// Switch to the first visible nav item
|
||||
var visibleNavs = document.querySelectorAll(
|
||||
'.admin-nav:not([style*="display: none"])',
|
||||
);
|
||||
if (visibleNavs.length > 0) {
|
||||
switchAdminTab(visibleNavs[0].getAttribute("data-tab"));
|
||||
} else {
|
||||
// No tabs visible — show empty state
|
||||
var panels = document.querySelectorAll(".admin-panel");
|
||||
for (var j = 0; j < panels.length; j++) panels[j].style.display = "none";
|
||||
var empty = document.getElementById("admin-no-permissions");
|
||||
@@ -75,23 +130,54 @@ function showAdmin() {
|
||||
empty.id = "admin-no-permissions";
|
||||
empty.className = "dashboard-empty";
|
||||
empty.textContent = "You do not have permissions to view any admin tabs.";
|
||||
document.getElementById("view-admin").appendChild(empty);
|
||||
document.getElementById("admin-content").appendChild(empty);
|
||||
}
|
||||
empty.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function _injectMobileToggle(tab) {
|
||||
var toggle = document.getElementById("admin-mobile-toggle");
|
||||
if (!toggle) {
|
||||
toggle = document.createElement("button");
|
||||
toggle.id = "admin-mobile-toggle";
|
||||
toggle.className = "admin-mobile-toggle";
|
||||
toggle.setAttribute("aria-label", "Open navigation");
|
||||
toggle.onclick = function () {
|
||||
_mobileSidebarOpen = false;
|
||||
_toggleMobileSidebar();
|
||||
};
|
||||
}
|
||||
var panel = document.getElementById("admin-" + tab);
|
||||
if (panel) {
|
||||
var toolbar = panel.querySelector(".admin-toolbar");
|
||||
if (toolbar) toolbar.insertBefore(toggle, toolbar.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function _toggleMobileSidebar() {
|
||||
_mobileSidebarOpen = !_mobileSidebarOpen;
|
||||
var sidebar = document.getElementById("admin-sidebar");
|
||||
sidebar.classList.toggle("open", _mobileSidebarOpen);
|
||||
sidebar.classList.toggle("collapsed", !_mobileSidebarOpen);
|
||||
sidebar.setAttribute("aria-hidden", _mobileSidebarOpen ? "false" : "true");
|
||||
if (_mobileSidebarOpen) sidebar.removeAttribute("inert");
|
||||
else sidebar.setAttribute("inert", "");
|
||||
var backdrop = document.getElementById("admin-sidebar-backdrop");
|
||||
if (backdrop) backdrop.classList.toggle("visible", _mobileSidebarOpen);
|
||||
}
|
||||
|
||||
function switchAdminTab(tab) {
|
||||
_adminTab = tab;
|
||||
// Hide no-permissions empty state if it was showing
|
||||
var noPerms = document.getElementById("admin-no-permissions");
|
||||
if (noPerms) noPerms.style.display = "none";
|
||||
var tabs = document.querySelectorAll(".admin-tab");
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
var isActive = tabs[i].getAttribute("data-tab") === tab;
|
||||
tabs[i].classList.toggle("active", isActive);
|
||||
tabs[i].setAttribute("aria-selected", isActive ? "true" : "false");
|
||||
tabs[i].setAttribute("tabindex", isActive ? "0" : "-1");
|
||||
var navItems = document.querySelectorAll(".admin-nav");
|
||||
for (var i = 0; i < navItems.length; i++) {
|
||||
var isActive = navItems[i].getAttribute("data-tab") === tab;
|
||||
navItems[i].classList.toggle("active", isActive);
|
||||
navItems[i].setAttribute("aria-selected", isActive ? "true" : "false");
|
||||
navItems[i].setAttribute("tabindex", isActive ? "0" : "-1");
|
||||
}
|
||||
var panels = [
|
||||
"users",
|
||||
@@ -105,6 +191,7 @@ function switchAdminTab(tab) {
|
||||
"ws-templates",
|
||||
"usage",
|
||||
"audit",
|
||||
"settings",
|
||||
];
|
||||
for (var p = 0; p < panels.length; p++) {
|
||||
var el = document.getElementById("admin-" + panels[p]);
|
||||
@@ -125,6 +212,21 @@ function switchAdminTab(tab) {
|
||||
_populateAuditUserFilter();
|
||||
loadGovAudit();
|
||||
}
|
||||
if (tab === "settings") loadSettings();
|
||||
|
||||
// Update breadcrumb with active tab label
|
||||
var activeNav = document.querySelector('.admin-nav[data-tab="' + tab + '"]');
|
||||
var label = activeNav ? activeNav.textContent : tab;
|
||||
var bcLabel = document.getElementById("breadcrumb-label");
|
||||
if (bcLabel) bcLabel.textContent = "Admin / " + label;
|
||||
|
||||
// Inject mobile hamburger toggle into active panel's toolbar
|
||||
_injectMobileToggle(tab);
|
||||
|
||||
// On mobile, auto-close sidebar after tab selection
|
||||
if (window.innerWidth <= 700 && _mobileSidebarOpen) {
|
||||
_toggleMobileSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1574,33 +1676,70 @@ document.addEventListener("keydown", function (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Close mobile sidebar drawer on Escape
|
||||
if (_mobileSidebarOpen && window.innerWidth <= 700) {
|
||||
e.preventDefault();
|
||||
_toggleMobileSidebar();
|
||||
var mt = document.getElementById("admin-mobile-toggle");
|
||||
if (mt) mt.focus();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Tab arrow key navigation
|
||||
// Sidebar arrow key navigation (vertical)
|
||||
(function () {
|
||||
var tablist = document.querySelector(".admin-tabs");
|
||||
if (!tablist) return;
|
||||
tablist.addEventListener("keydown", function (e) {
|
||||
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
||||
var allTabs = document.querySelectorAll(
|
||||
'.admin-tab:not([style*="display: none"])',
|
||||
var sidebar = document.getElementById("admin-sidebar");
|
||||
if (!sidebar) return;
|
||||
sidebar.addEventListener("keydown", function (e) {
|
||||
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
||||
e.preventDefault();
|
||||
var allNavs = document.querySelectorAll(
|
||||
'.admin-nav:not([style*="display: none"])',
|
||||
);
|
||||
var tabOrder = [];
|
||||
for (var ti = 0; ti < allTabs.length; ti++) {
|
||||
tabOrder.push(allTabs[ti].getAttribute("data-tab"));
|
||||
var navOrder = [];
|
||||
for (var ni = 0; ni < allNavs.length; ni++) {
|
||||
navOrder.push(allNavs[ni].getAttribute("data-tab"));
|
||||
}
|
||||
if (tabOrder.length === 0) return;
|
||||
var idx = tabOrder.indexOf(_adminTab);
|
||||
if (e.key === "ArrowRight") idx = (idx + 1) % tabOrder.length;
|
||||
else idx = (idx - 1 + tabOrder.length) % tabOrder.length;
|
||||
switchAdminTab(tabOrder[idx]);
|
||||
if (navOrder.length === 0) return;
|
||||
var idx = navOrder.indexOf(_adminTab);
|
||||
if (e.key === "ArrowDown") idx = (idx + 1) % navOrder.length;
|
||||
else idx = (idx - 1 + navOrder.length) % navOrder.length;
|
||||
switchAdminTab(navOrder[idx]);
|
||||
var btn = document.querySelector(
|
||||
'.admin-tab[data-tab="' + tabOrder[idx] + '"]',
|
||||
'.admin-nav[data-tab="' + navOrder[idx] + '"]',
|
||||
);
|
||||
if (btn) btn.focus();
|
||||
});
|
||||
})();
|
||||
|
||||
// Sync sidebar aria-hidden/inert when crossing mobile/desktop breakpoint
|
||||
(function () {
|
||||
var resizeTimer;
|
||||
window.addEventListener("resize", function () {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
if (typeof currentView === "undefined" || currentView !== "admin") return;
|
||||
var sidebar = document.getElementById("admin-sidebar");
|
||||
if (!sidebar) return;
|
||||
var isMobile = window.innerWidth <= 700;
|
||||
var backdrop = document.getElementById("admin-sidebar-backdrop");
|
||||
if (isMobile && !_mobileSidebarOpen) {
|
||||
sidebar.setAttribute("aria-hidden", "true");
|
||||
sidebar.setAttribute("inert", "");
|
||||
sidebar.classList.add("collapsed");
|
||||
sidebar.classList.remove("open");
|
||||
if (backdrop) backdrop.classList.remove("visible");
|
||||
} else if (!isMobile) {
|
||||
sidebar.removeAttribute("aria-hidden");
|
||||
sidebar.removeAttribute("inert");
|
||||
sidebar.classList.remove("collapsed", "open");
|
||||
if (backdrop) backdrop.classList.remove("visible");
|
||||
_mobileSidebarOpen = false;
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
})();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Confirm Modal (reusable styled replacement for confirm())
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1644,6 +1783,16 @@ function _confirmCallback() {
|
||||
hideConfirmModal();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings (stub — full implementation is a separate project)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadSettings() {
|
||||
var el = document.getElementById("admin-settings-content");
|
||||
if (el)
|
||||
el.innerHTML = '<div class="dashboard-empty">Settings coming soon</div>';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -383,6 +383,11 @@ function showOverview() {
|
||||
document.getElementById("view-filtered").style.display = "none";
|
||||
var adminView = document.getElementById("view-admin");
|
||||
if (adminView) adminView.style.display = "none";
|
||||
var adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.remove("active");
|
||||
adminBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
document.getElementById("breadcrumb").style.display = "none";
|
||||
document.getElementById("main").scrollTop = 0;
|
||||
if (clusterState) renderFromState();
|
||||
@@ -924,6 +929,11 @@ function drillDownToNode(nodeId, serverUrl) {
|
||||
document.getElementById("view-filtered").style.display = "none";
|
||||
var adminView = document.getElementById("view-admin");
|
||||
if (adminView) adminView.style.display = "none";
|
||||
var adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.remove("active");
|
||||
adminBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
document.getElementById("breadcrumb").style.display = "";
|
||||
document.getElementById("breadcrumb-label").textContent = nodeId;
|
||||
var link = document.getElementById("node-link");
|
||||
@@ -973,6 +983,11 @@ function drillDownByState(state) {
|
||||
document.getElementById("view-filtered").style.display = "";
|
||||
var adminView = document.getElementById("view-admin");
|
||||
if (adminView) adminView.style.display = "none";
|
||||
var adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.remove("active");
|
||||
adminBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
document.getElementById("breadcrumb").style.display = "";
|
||||
var sd = STATE_DISPLAY[state] || STATE_DISPLAY.idle;
|
||||
document.getElementById("breadcrumb-label").textContent =
|
||||
@@ -995,6 +1010,11 @@ function drillDownByNode(nodeId) {
|
||||
document.getElementById("view-filtered").style.display = "";
|
||||
var adminView = document.getElementById("view-admin");
|
||||
if (adminView) adminView.style.display = "none";
|
||||
var adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) {
|
||||
adminBtn.classList.remove("active");
|
||||
adminBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
document.getElementById("breadcrumb").style.display = "";
|
||||
document.getElementById("breadcrumb-label").textContent = nodeId;
|
||||
document.getElementById("filtered-title").textContent =
|
||||
|
||||
+234
-167
@@ -16,7 +16,7 @@
|
||||
<span id="cluster-summary" aria-live="polite"></span>
|
||||
<span id="status-bar" role="status" aria-live="polite"></span>
|
||||
<button id="new-ws-btn" class="header-btn header-btn-accent" onclick="showNewWsModal()" title="Create workstream">+ new</button>
|
||||
<button id="admin-btn" class="header-btn" onclick="showAdmin()" title="User & token administration">admin</button>
|
||||
<button id="admin-btn" class="header-btn" onclick="showAdmin()" title="User & token administration" aria-expanded="false" aria-controls="view-admin">admin</button>
|
||||
<button id="logout-btn" class="header-btn" onclick="logout()" style="display:none">logout</button>
|
||||
<button id="theme-toggle" class="header-btn" onclick="toggleTheme()" aria-label="Toggle light/dark theme">☾</button>
|
||||
</div>
|
||||
@@ -77,19 +77,41 @@
|
||||
|
||||
<!-- ADMIN PANEL -->
|
||||
<div id="view-admin" style="display:none">
|
||||
<div class="admin-tabs" role="tablist">
|
||||
<button id="tab-users" class="admin-tab active" data-tab="users" role="tab" aria-selected="true" aria-controls="admin-users" tabindex="0" onclick="switchAdminTab('users')">Users</button>
|
||||
<button id="tab-tokens" class="admin-tab" data-tab="tokens" role="tab" aria-selected="false" aria-controls="admin-tokens" tabindex="-1" onclick="switchAdminTab('tokens')">Tokens</button>
|
||||
<button id="tab-channels" class="admin-tab" data-tab="channels" role="tab" aria-selected="false" aria-controls="admin-channels" tabindex="-1" onclick="switchAdminTab('channels')">Channels</button>
|
||||
<button id="tab-schedules" class="admin-tab" data-tab="schedules" role="tab" aria-selected="false" aria-controls="admin-schedules" tabindex="-1" onclick="switchAdminTab('schedules')">Schedules</button>
|
||||
<button id="tab-watches" class="admin-tab" data-tab="watches" role="tab" aria-selected="false" aria-controls="admin-watches" tabindex="-1" onclick="switchAdminTab('watches')">Watches</button>
|
||||
<button id="tab-roles" class="admin-tab" data-tab="roles" role="tab" aria-selected="false" aria-controls="admin-roles" tabindex="-1" onclick="switchAdminTab('roles')">Roles</button>
|
||||
<button id="tab-policies" class="admin-tab" data-tab="policies" role="tab" aria-selected="false" aria-controls="admin-policies" tabindex="-1" onclick="switchAdminTab('policies')">Policies</button>
|
||||
<button id="tab-templates" class="admin-tab" data-tab="templates" role="tab" aria-selected="false" aria-controls="admin-templates" tabindex="-1" onclick="switchAdminTab('templates')">Templates</button>
|
||||
<button id="tab-ws-templates" class="admin-tab" data-tab="ws-templates" role="tab" aria-selected="false" aria-controls="admin-ws-templates" tabindex="-1" onclick="switchAdminTab('ws-templates')">WS Templates</button>
|
||||
<button id="tab-usage" class="admin-tab" data-tab="usage" role="tab" aria-selected="false" aria-controls="admin-usage" tabindex="-1" onclick="switchAdminTab('usage')">Usage</button>
|
||||
<button id="tab-audit" class="admin-tab" data-tab="audit" role="tab" aria-selected="false" aria-controls="admin-audit" tabindex="-1" onclick="switchAdminTab('audit')">Audit</button>
|
||||
</div>
|
||||
<div id="admin-layout" class="admin-layout">
|
||||
<!-- Sidebar navigation -->
|
||||
<nav id="admin-sidebar" class="admin-sidebar" role="tablist" aria-label="Admin navigation" aria-orientation="vertical">
|
||||
<div class="admin-sidebar-group" data-group="identity" role="group" aria-label="Identity">
|
||||
<div class="admin-sidebar-group-label" aria-hidden="true">Identity</div>
|
||||
<button id="tab-users" class="admin-nav active" data-tab="users" role="tab" aria-selected="true" aria-controls="admin-users" tabindex="0" onclick="switchAdminTab('users')">Users</button>
|
||||
<button id="tab-tokens" class="admin-nav" data-tab="tokens" role="tab" aria-selected="false" aria-controls="admin-tokens" tabindex="-1" onclick="switchAdminTab('tokens')">API Tokens</button>
|
||||
<button id="tab-channels" class="admin-nav" data-tab="channels" role="tab" aria-selected="false" aria-controls="admin-channels" tabindex="-1" onclick="switchAdminTab('channels')">Channels</button>
|
||||
</div>
|
||||
<div class="admin-sidebar-group" data-group="automation" role="group" aria-label="Automation">
|
||||
<div class="admin-sidebar-group-label" aria-hidden="true">Automation</div>
|
||||
<button id="tab-schedules" class="admin-nav" data-tab="schedules" role="tab" aria-selected="false" aria-controls="admin-schedules" tabindex="-1" onclick="switchAdminTab('schedules')">Schedules</button>
|
||||
<button id="tab-watches" class="admin-nav" data-tab="watches" role="tab" aria-selected="false" aria-controls="admin-watches" tabindex="-1" onclick="switchAdminTab('watches')">Watches</button>
|
||||
</div>
|
||||
<div class="admin-sidebar-group" data-group="governance" role="group" aria-label="Governance">
|
||||
<div class="admin-sidebar-group-label" aria-hidden="true">Governance</div>
|
||||
<button id="tab-roles" class="admin-nav" data-tab="roles" role="tab" aria-selected="false" aria-controls="admin-roles" tabindex="-1" onclick="switchAdminTab('roles')">Roles</button>
|
||||
<button id="tab-policies" class="admin-nav" data-tab="policies" role="tab" aria-selected="false" aria-controls="admin-policies" tabindex="-1" onclick="switchAdminTab('policies')">Policies</button>
|
||||
<button id="tab-templates" class="admin-nav" data-tab="templates" role="tab" aria-selected="false" aria-controls="admin-templates" tabindex="-1" onclick="switchAdminTab('templates')">Templates</button>
|
||||
<button id="tab-ws-templates" class="admin-nav" data-tab="ws-templates" role="tab" aria-selected="false" aria-controls="admin-ws-templates" tabindex="-1" onclick="switchAdminTab('ws-templates')">WS Templates</button>
|
||||
</div>
|
||||
<div class="admin-sidebar-group" data-group="observe" role="group" aria-label="Observe">
|
||||
<div class="admin-sidebar-group-label" aria-hidden="true">Observe</div>
|
||||
<button id="tab-usage" class="admin-nav" data-tab="usage" role="tab" aria-selected="false" aria-controls="admin-usage" tabindex="-1" onclick="switchAdminTab('usage')">Usage</button>
|
||||
<button id="tab-audit" class="admin-nav" data-tab="audit" role="tab" aria-selected="false" aria-controls="admin-audit" tabindex="-1" onclick="switchAdminTab('audit')">Audit</button>
|
||||
</div>
|
||||
<div class="admin-sidebar-group" data-group="system" role="group" aria-label="System">
|
||||
<div class="admin-sidebar-group-label" aria-hidden="true">System</div>
|
||||
<button id="tab-settings" class="admin-nav" data-tab="settings" role="tab" aria-selected="false" aria-controls="admin-settings" tabindex="-1" onclick="switchAdminTab('settings')">Settings</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="admin-sidebar-backdrop" class="admin-sidebar-backdrop" aria-hidden="true"></div>
|
||||
|
||||
<!-- Content area -->
|
||||
<div id="admin-content" class="admin-content">
|
||||
|
||||
<!-- Users Tab -->
|
||||
<div id="admin-users" class="admin-panel" role="tabpanel" aria-labelledby="tab-users">
|
||||
@@ -111,7 +133,7 @@
|
||||
<!-- Tokens Tab -->
|
||||
<div id="admin-tokens" class="admin-panel" role="tabpanel" aria-labelledby="tab-tokens" style="display:none">
|
||||
<div class="admin-toolbar">
|
||||
<span class="section-header" style="margin:0">TOKENS</span>
|
||||
<span class="section-header" style="margin:0">API TOKENS</span>
|
||||
<label for="admin-token-user" class="sr-only">Filter tokens by user</label>
|
||||
<select id="admin-token-user" onchange="loadAdminTokens()">
|
||||
<option value="">Select user...</option>
|
||||
@@ -324,6 +346,19 @@
|
||||
<div class="dashboard-empty">Loading audit log...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div id="admin-settings" class="admin-panel" role="tabpanel" aria-labelledby="tab-settings" style="display:none">
|
||||
<div class="admin-toolbar">
|
||||
<span class="section-header" style="margin:0">SETTINGS</span>
|
||||
</div>
|
||||
<div id="admin-settings-content">
|
||||
<div class="dashboard-empty">Settings coming soon</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /admin-content -->
|
||||
</div><!-- /admin-layout -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -479,44 +514,52 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
<div id="create-schedule-box" class="admin-modal admin-modal-wide">
|
||||
<h2 id="create-schedule-title">New Schedule</h2>
|
||||
<div id="create-schedule-error" role="alert" aria-live="assertive"></div>
|
||||
<label for="cs-name">Name</label>
|
||||
<input id="cs-name" type="text" placeholder="Daily health check" autocomplete="off">
|
||||
<label for="cs-desc">Description <span class="label-hint">optional</span></label>
|
||||
<input id="cs-desc" type="text" placeholder="" autocomplete="off">
|
||||
<label for="cs-type">Schedule type</label>
|
||||
<select id="cs-type" onchange="toggleScheduleTypeFields()">
|
||||
<option value="cron">Cron (recurring)</option>
|
||||
<option value="at">At (one-shot)</option>
|
||||
</select>
|
||||
<div id="cs-cron-group">
|
||||
<label for="cs-cron">Cron expression</label>
|
||||
<input id="cs-cron" type="text" placeholder="0 9 * * MON-FRI" autocomplete="off" spellcheck="false" aria-describedby="cs-cron-hint">
|
||||
<span id="cs-cron-hint" class="label-hint" style="display:block;margin-top:3px">min hour day month weekday</span>
|
||||
<div class="modal-columns">
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Schedule</div>
|
||||
<label for="cs-name">Name</label>
|
||||
<input id="cs-name" type="text" placeholder="Daily health check" autocomplete="off">
|
||||
<label for="cs-desc">Description <span class="label-hint">optional</span></label>
|
||||
<input id="cs-desc" type="text" placeholder="" autocomplete="off">
|
||||
<label for="cs-type">Schedule type</label>
|
||||
<select id="cs-type" onchange="toggleScheduleTypeFields()">
|
||||
<option value="cron">Cron (recurring)</option>
|
||||
<option value="at">At (one-shot)</option>
|
||||
</select>
|
||||
<div id="cs-cron-group">
|
||||
<label for="cs-cron">Cron expression</label>
|
||||
<input id="cs-cron" type="text" placeholder="0 9 * * MON-FRI" autocomplete="off" spellcheck="false" aria-describedby="cs-cron-hint">
|
||||
<span id="cs-cron-hint" class="label-hint" style="display:block;margin-top:3px">min hour day month weekday</span>
|
||||
</div>
|
||||
<div id="cs-at-group" style="display:none">
|
||||
<label for="cs-at">Run at</label>
|
||||
<input id="cs-at" type="datetime-local">
|
||||
</div>
|
||||
<label for="cs-target">Target</label>
|
||||
<select id="cs-target" onchange="toggleScheduleNodeField()">
|
||||
<option value="auto">Auto (best available)</option>
|
||||
<option value="pool">Pool (any bridge)</option>
|
||||
<option value="all">All nodes</option>
|
||||
<option value="node">Specific node...</option>
|
||||
</select>
|
||||
<div id="cs-node-group" style="display:none">
|
||||
<label for="cs-node">Node ID</label>
|
||||
<input id="cs-node" type="text" placeholder="node-001" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Execution</div>
|
||||
<label for="cs-model">Model <span class="label-hint">optional</span></label>
|
||||
<input id="cs-model" type="text" placeholder="Default model" autocomplete="off">
|
||||
<label for="cs-template">Template <span class="label-hint">optional</span></label>
|
||||
<input id="cs-template" type="text" placeholder="Prompt template name" autocomplete="off">
|
||||
<label for="cs-ws-template">WS Template <span class="label-hint">optional</span></label>
|
||||
<select id="cs-ws-template"><option value="">None</option></select>
|
||||
<label for="cs-message">Initial message</label>
|
||||
<textarea id="cs-message" rows="3" placeholder="What should the workstream do?"></textarea>
|
||||
<label class="admin-checkbox"><input id="cs-autoapprove" type="checkbox"> Auto-approve tool calls</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cs-at-group" style="display:none">
|
||||
<label for="cs-at">Run at</label>
|
||||
<input id="cs-at" type="datetime-local">
|
||||
</div>
|
||||
<label for="cs-target">Target</label>
|
||||
<select id="cs-target" onchange="toggleScheduleNodeField()">
|
||||
<option value="auto">Auto (best available)</option>
|
||||
<option value="pool">Pool (any bridge)</option>
|
||||
<option value="all">All nodes</option>
|
||||
<option value="node">Specific node...</option>
|
||||
</select>
|
||||
<div id="cs-node-group" style="display:none">
|
||||
<label for="cs-node">Node ID</label>
|
||||
<input id="cs-node" type="text" placeholder="node-001" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<label for="cs-model">Model <span class="label-hint">optional</span></label>
|
||||
<input id="cs-model" type="text" placeholder="Default model" autocomplete="off">
|
||||
<label for="cs-template">Template <span class="label-hint">optional</span></label>
|
||||
<input id="cs-template" type="text" placeholder="Prompt template name" autocomplete="off">
|
||||
<label for="cs-ws-template">WS Template <span class="label-hint">optional — workstream profile</span></label>
|
||||
<select id="cs-ws-template"><option value="">None</option></select>
|
||||
<label for="cs-message">Initial message</label>
|
||||
<textarea id="cs-message" rows="3" placeholder="What should the workstream do?"></textarea>
|
||||
<label class="admin-checkbox"><input id="cs-autoapprove" type="checkbox"> Auto-approve tool calls</label>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideCreateScheduleModal()">Cancel</button>
|
||||
<button id="cs-submit" class="modal-submit" onclick="submitCreateSchedule()">Create</button>
|
||||
@@ -530,44 +573,52 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
<h2 id="edit-schedule-title">Edit Schedule</h2>
|
||||
<div id="edit-schedule-error" role="alert" aria-live="assertive"></div>
|
||||
<input id="es-id" type="hidden">
|
||||
<label for="es-name">Name</label>
|
||||
<input id="es-name" type="text" autocomplete="off">
|
||||
<label for="es-desc">Description</label>
|
||||
<input id="es-desc" type="text" autocomplete="off">
|
||||
<label for="es-type">Schedule type</label>
|
||||
<select id="es-type" onchange="toggleEditScheduleTypeFields()">
|
||||
<option value="cron">Cron (recurring)</option>
|
||||
<option value="at">At (one-shot)</option>
|
||||
</select>
|
||||
<div id="es-cron-group">
|
||||
<label for="es-cron">Cron expression</label>
|
||||
<input id="es-cron" type="text" autocomplete="off" spellcheck="false">
|
||||
<div class="modal-columns">
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Schedule</div>
|
||||
<label for="es-name">Name</label>
|
||||
<input id="es-name" type="text" autocomplete="off">
|
||||
<label for="es-desc">Description</label>
|
||||
<input id="es-desc" type="text" autocomplete="off">
|
||||
<label for="es-type">Schedule type</label>
|
||||
<select id="es-type" onchange="toggleEditScheduleTypeFields()">
|
||||
<option value="cron">Cron (recurring)</option>
|
||||
<option value="at">At (one-shot)</option>
|
||||
</select>
|
||||
<div id="es-cron-group">
|
||||
<label for="es-cron">Cron expression</label>
|
||||
<input id="es-cron" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div id="es-at-group" style="display:none">
|
||||
<label for="es-at">Run at</label>
|
||||
<input id="es-at" type="datetime-local">
|
||||
</div>
|
||||
<label for="es-target">Target</label>
|
||||
<select id="es-target" onchange="toggleEditScheduleNodeField()">
|
||||
<option value="auto">Auto (best available)</option>
|
||||
<option value="pool">Pool (any bridge)</option>
|
||||
<option value="all">All nodes</option>
|
||||
<option value="node">Specific node...</option>
|
||||
</select>
|
||||
<div id="es-node-group" style="display:none">
|
||||
<label for="es-node">Node ID</label>
|
||||
<input id="es-node" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Execution</div>
|
||||
<label for="es-model">Model</label>
|
||||
<input id="es-model" type="text" autocomplete="off">
|
||||
<label for="es-template">Template <span class="label-hint">optional</span></label>
|
||||
<input id="es-template" type="text" autocomplete="off">
|
||||
<label for="es-ws-template">WS Template <span class="label-hint">optional</span></label>
|
||||
<select id="es-ws-template"><option value="">None</option></select>
|
||||
<label for="es-message">Initial message</label>
|
||||
<textarea id="es-message" rows="3"></textarea>
|
||||
<label class="admin-checkbox"><input id="es-autoapprove" type="checkbox"> Auto-approve tool calls</label>
|
||||
<label class="admin-checkbox"><input id="es-enabled" type="checkbox"> Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="es-at-group" style="display:none">
|
||||
<label for="es-at">Run at</label>
|
||||
<input id="es-at" type="datetime-local">
|
||||
</div>
|
||||
<label for="es-target">Target</label>
|
||||
<select id="es-target" onchange="toggleEditScheduleNodeField()">
|
||||
<option value="auto">Auto (best available)</option>
|
||||
<option value="pool">Pool (any bridge)</option>
|
||||
<option value="all">All nodes</option>
|
||||
<option value="node">Specific node...</option>
|
||||
</select>
|
||||
<div id="es-node-group" style="display:none">
|
||||
<label for="es-node">Node ID</label>
|
||||
<input id="es-node" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<label for="es-model">Model</label>
|
||||
<input id="es-model" type="text" autocomplete="off">
|
||||
<label for="es-template">Template <span class="label-hint">optional</span></label>
|
||||
<input id="es-template" type="text" autocomplete="off">
|
||||
<label for="es-ws-template">WS Template <span class="label-hint">optional</span></label>
|
||||
<select id="es-ws-template"><option value="">None</option></select>
|
||||
<label for="es-message">Initial message</label>
|
||||
<textarea id="es-message" rows="3"></textarea>
|
||||
<label class="admin-checkbox"><input id="es-autoapprove" type="checkbox"> Auto-approve tool calls</label>
|
||||
<label class="admin-checkbox"><input id="es-enabled" type="checkbox"> Enabled</label>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideEditScheduleModal()">Cancel</button>
|
||||
<button id="es-submit" class="modal-submit" onclick="submitEditSchedule()">Save</button>
|
||||
@@ -745,46 +796,54 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
<div id="create-wst-box" class="admin-modal admin-modal-wide">
|
||||
<h2 id="create-wst-title">Create Workstream Template</h2>
|
||||
<div id="create-wst-error" role="alert" aria-live="assertive"></div>
|
||||
<label for="cwst-name">Name</label>
|
||||
<input id="cwst-name" type="text" placeholder="e.g. Code Review Agent" autocomplete="off">
|
||||
<label for="cwst-description">Description <span class="label-hint">optional</span></label>
|
||||
<input id="cwst-description" type="text" placeholder="Brief description" autocomplete="off">
|
||||
<label>System Prompt Source</label>
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-inline" type="radio" name="cwst-src" value="inline" checked onchange="toggleWstPromptSource()"> Inline</label>
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-ref" type="radio" name="cwst-src" value="ref" onchange="toggleWstPromptSource()"> Prompt Template</label>
|
||||
<div class="modal-columns">
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Identity</div>
|
||||
<label for="cwst-name">Name</label>
|
||||
<input id="cwst-name" type="text" placeholder="e.g. Code Review Agent" autocomplete="off">
|
||||
<label for="cwst-description">Description <span class="label-hint">optional</span></label>
|
||||
<input id="cwst-description" type="text" placeholder="Brief description" autocomplete="off">
|
||||
<label>System Prompt Source</label>
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-inline" type="radio" name="cwst-src" value="inline" checked onchange="toggleWstPromptSource()"> Inline</label>
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-ref" type="radio" name="cwst-src" value="ref" onchange="toggleWstPromptSource()"> Prompt Template</label>
|
||||
</div>
|
||||
<div id="cwst-inline-section">
|
||||
<label for="cwst-system-prompt">System Prompt <span class="label-hint">inline text</span></label>
|
||||
<textarea id="cwst-system-prompt" rows="4" placeholder="You are a..."></textarea>
|
||||
</div>
|
||||
<div id="cwst-ref-section" style="display:none">
|
||||
<label for="cwst-prompt-template">Prompt Template <span class="label-hint">reference by name</span></label>
|
||||
<select id="cwst-prompt-template"><option value="">None</option></select>
|
||||
</div>
|
||||
<label class="admin-checkbox"><input id="cwst-auto-approve" type="checkbox"> Auto-approve all tools</label>
|
||||
<label for="cwst-auto-approve-tools">Auto-approve tools <span class="label-hint">comma-separated</span></label>
|
||||
<input id="cwst-auto-approve-tools" type="text" placeholder="e.g. read_file, list_directory" autocomplete="off">
|
||||
</div>
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Model Config</div>
|
||||
<label for="cwst-model">Model</label>
|
||||
<input id="cwst-model" type="text" autocomplete="off">
|
||||
<label for="cwst-temperature">Temperature <span class="label-hint">0.0–2.0</span></label>
|
||||
<input id="cwst-temperature" type="number" step="0.1" min="0" max="2" autocomplete="off">
|
||||
<label for="cwst-reasoning-effort">Reasoning effort</label>
|
||||
<select id="cwst-reasoning-effort">
|
||||
<option value="">Default</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="none">None</option>
|
||||
<option value="max">Max</option>
|
||||
</select>
|
||||
<label for="cwst-max-tokens">Max tokens <span class="label-hint">0 = default</span></label>
|
||||
<input id="cwst-max-tokens" type="number" min="0" autocomplete="off">
|
||||
<label for="cwst-agent-max-turns">Agent max turns <span class="label-hint">0 = default</span></label>
|
||||
<input id="cwst-agent-max-turns" type="number" min="0" autocomplete="off">
|
||||
<label for="cwst-token-budget">Token budget <span class="label-hint">0 = unlimited</span></label>
|
||||
<input id="cwst-token-budget" type="number" value="0" min="0">
|
||||
<label class="admin-checkbox"><input id="cwst-enabled" type="checkbox" checked> Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cwst-inline-section">
|
||||
<label for="cwst-system-prompt">System Prompt <span class="label-hint">inline text</span></label>
|
||||
<textarea id="cwst-system-prompt" rows="4" placeholder="You are a..."></textarea>
|
||||
</div>
|
||||
<div id="cwst-ref-section" style="display:none">
|
||||
<label for="cwst-prompt-template">Prompt Template <span class="label-hint">reference by name</span></label>
|
||||
<select id="cwst-prompt-template"><option value="">None</option></select>
|
||||
</div>
|
||||
<label for="cwst-model">Model <span class="label-hint">optional — server default if empty</span></label>
|
||||
<input id="cwst-model" type="text" placeholder="Default model" autocomplete="off">
|
||||
<label class="admin-checkbox"><input id="cwst-auto-approve" type="checkbox"> Auto-approve all tools</label>
|
||||
<label for="cwst-auto-approve-tools">Auto-approve tools <span class="label-hint">comma-separated tool names</span></label>
|
||||
<input id="cwst-auto-approve-tools" type="text" placeholder="e.g. read_file, list_directory" autocomplete="off">
|
||||
<label for="cwst-token-budget">Token budget <span class="label-hint">0 = unlimited</span></label>
|
||||
<input id="cwst-token-budget" type="number" value="0" min="0">
|
||||
<label for="cwst-temperature">Temperature <span class="label-hint">optional — 0.0-2.0, empty = server default</span></label>
|
||||
<input id="cwst-temperature" type="number" step="0.1" min="0" max="2" placeholder="Server default" autocomplete="off">
|
||||
<label for="cwst-reasoning-effort">Reasoning effort <span class="label-hint">optional</span></label>
|
||||
<select id="cwst-reasoning-effort">
|
||||
<option value="">Server default</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="none">None</option>
|
||||
<option value="max">Max</option>
|
||||
</select>
|
||||
<label for="cwst-max-tokens">Max tokens <span class="label-hint">optional — 0 = server default</span></label>
|
||||
<input id="cwst-max-tokens" type="number" min="0" placeholder="Server default" autocomplete="off">
|
||||
<label for="cwst-agent-max-turns">Agent max turns <span class="label-hint">optional — 0 = server default</span></label>
|
||||
<input id="cwst-agent-max-turns" type="number" min="0" placeholder="Server default" autocomplete="off">
|
||||
<label class="admin-checkbox"><input id="cwst-enabled" type="checkbox" checked> Enabled</label>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideCreateWsTemplateModal()">Cancel</button>
|
||||
<button id="cwst-submit" class="modal-submit" onclick="submitCreateWsTemplate()">Create</button>
|
||||
@@ -798,46 +857,54 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
<h2 id="edit-wst-title">Edit Workstream Template</h2>
|
||||
<div id="edit-wst-error" role="alert" aria-live="assertive"></div>
|
||||
<input id="ewst-id" type="hidden">
|
||||
<label for="ewst-name">Name</label>
|
||||
<input id="ewst-name" type="text" autocomplete="off">
|
||||
<label for="ewst-description">Description</label>
|
||||
<input id="ewst-description" type="text" autocomplete="off">
|
||||
<label>System Prompt Source</label>
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-inline" type="radio" name="ewst-src" value="inline" checked onchange="toggleEditWstPromptSource()"> Inline</label>
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-ref" type="radio" name="ewst-src" value="ref" onchange="toggleEditWstPromptSource()"> Prompt Template</label>
|
||||
<div class="modal-columns">
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Identity</div>
|
||||
<label for="ewst-name">Name</label>
|
||||
<input id="ewst-name" type="text" autocomplete="off">
|
||||
<label for="ewst-description">Description</label>
|
||||
<input id="ewst-description" type="text" autocomplete="off">
|
||||
<label>System Prompt Source</label>
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-inline" type="radio" name="ewst-src" value="inline" checked onchange="toggleEditWstPromptSource()"> Inline</label>
|
||||
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-ref" type="radio" name="ewst-src" value="ref" onchange="toggleEditWstPromptSource()"> Prompt Template</label>
|
||||
</div>
|
||||
<div id="ewst-inline-section">
|
||||
<label for="ewst-system-prompt">System Prompt</label>
|
||||
<textarea id="ewst-system-prompt" rows="4"></textarea>
|
||||
</div>
|
||||
<div id="ewst-ref-section" style="display:none">
|
||||
<label for="ewst-prompt-template">Prompt Template</label>
|
||||
<select id="ewst-prompt-template"><option value="">None</option></select>
|
||||
</div>
|
||||
<label class="admin-checkbox"><input id="ewst-auto-approve" type="checkbox"> Auto-approve all tools</label>
|
||||
<label for="ewst-auto-approve-tools">Auto-approve tools</label>
|
||||
<input id="ewst-auto-approve-tools" type="text" autocomplete="off">
|
||||
</div>
|
||||
<div class="modal-col">
|
||||
<div class="modal-col-heading">Model Config</div>
|
||||
<label for="ewst-model">Model</label>
|
||||
<input id="ewst-model" type="text" autocomplete="off">
|
||||
<label for="ewst-temperature">Temperature <span class="label-hint">0.0–2.0</span></label>
|
||||
<input id="ewst-temperature" type="number" step="0.1" min="0" max="2" autocomplete="off">
|
||||
<label for="ewst-reasoning-effort">Reasoning effort</label>
|
||||
<select id="ewst-reasoning-effort">
|
||||
<option value="">Default</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="none">None</option>
|
||||
<option value="max">Max</option>
|
||||
</select>
|
||||
<label for="ewst-max-tokens">Max tokens <span class="label-hint">0 = default</span></label>
|
||||
<input id="ewst-max-tokens" type="number" min="0" autocomplete="off">
|
||||
<label for="ewst-agent-max-turns">Agent max turns <span class="label-hint">0 = default</span></label>
|
||||
<input id="ewst-agent-max-turns" type="number" min="0" autocomplete="off">
|
||||
<label for="ewst-token-budget">Token budget <span class="label-hint">0 = unlimited</span></label>
|
||||
<input id="ewst-token-budget" type="number" value="0" min="0">
|
||||
<label class="admin-checkbox"><input id="ewst-enabled" type="checkbox" checked> Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ewst-inline-section">
|
||||
<label for="ewst-system-prompt">System Prompt</label>
|
||||
<textarea id="ewst-system-prompt" rows="4"></textarea>
|
||||
</div>
|
||||
<div id="ewst-ref-section" style="display:none">
|
||||
<label for="ewst-prompt-template">Prompt Template</label>
|
||||
<select id="ewst-prompt-template"><option value="">None</option></select>
|
||||
</div>
|
||||
<label for="ewst-model">Model</label>
|
||||
<input id="ewst-model" type="text" autocomplete="off">
|
||||
<label class="admin-checkbox"><input id="ewst-auto-approve" type="checkbox"> Auto-approve all tools</label>
|
||||
<label for="ewst-auto-approve-tools">Auto-approve tools</label>
|
||||
<input id="ewst-auto-approve-tools" type="text" autocomplete="off">
|
||||
<label for="ewst-token-budget">Token budget</label>
|
||||
<input id="ewst-token-budget" type="number" value="0" min="0">
|
||||
<label for="ewst-temperature">Temperature <span class="label-hint">optional — 0.0-2.0, empty = server default</span></label>
|
||||
<input id="ewst-temperature" type="number" step="0.1" min="0" max="2" placeholder="Server default" autocomplete="off">
|
||||
<label for="ewst-reasoning-effort">Reasoning effort <span class="label-hint">optional</span></label>
|
||||
<select id="ewst-reasoning-effort">
|
||||
<option value="">Server default</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="none">None</option>
|
||||
<option value="max">Max</option>
|
||||
</select>
|
||||
<label for="ewst-max-tokens">Max tokens <span class="label-hint">optional</span></label>
|
||||
<input id="ewst-max-tokens" type="number" min="0" placeholder="Server default" autocomplete="off">
|
||||
<label for="ewst-agent-max-turns">Agent max turns <span class="label-hint">optional</span></label>
|
||||
<input id="ewst-agent-max-turns" type="number" min="0" placeholder="Server default" autocomplete="off">
|
||||
<label class="admin-checkbox"><input id="ewst-enabled" type="checkbox" checked> Enabled</label>
|
||||
<div class="modal-buttons">
|
||||
<button class="modal-cancel" onclick="hideEditWsTemplateModal()">Cancel</button>
|
||||
<button id="ewst-submit" class="modal-submit" onclick="submitEditWsTemplate()">Save</button>
|
||||
|
||||
@@ -543,6 +543,30 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Admin button — top accent line + active state */
|
||||
#admin-btn {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
#admin-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
border-radius: 0 0 1px 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
#admin-btn.active::before { opacity: 1; }
|
||||
#admin-btn.active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
New Workstream Modal
|
||||
========================================================================== */
|
||||
@@ -702,41 +726,126 @@
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Admin panel
|
||||
Admin panel — sidebar layout
|
||||
========================================================================== */
|
||||
|
||||
.admin-tabs {
|
||||
#view-admin { animation: admin-fadein 0.15s ease-out; }
|
||||
@keyframes admin-fadein { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.admin-tab {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--fg-dim);
|
||||
|
||||
/* Sidebar — right-aligned to match header admin button position */
|
||||
.admin-sidebar {
|
||||
width: 180px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-strong);
|
||||
padding: 4px 0;
|
||||
overflow-y: auto;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
/* Group labels */
|
||||
.admin-sidebar-group { margin-bottom: 2px; }
|
||||
.admin-sidebar-group-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 8px 16px 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--fg-dim);
|
||||
padding: 12px 16px 4px;
|
||||
}
|
||||
.admin-sidebar-group:first-child .admin-sidebar-group-label {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* Nav items */
|
||||
.admin-nav {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
border-right: 2px solid transparent;
|
||||
color: var(--fg-dim);
|
||||
font-family: var(--font-display);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 6px 14px 6px 16px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
text-align: left;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.admin-tab:hover { color: var(--fg); }
|
||||
.admin-tab.active {
|
||||
.admin-nav:hover { color: var(--fg); background: var(--bg-highlight); border-right-color: var(--border-strong); }
|
||||
.admin-nav.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
border-right-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
.admin-tab:focus-visible {
|
||||
.admin-nav:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Content area */
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
/* Mobile backdrop */
|
||||
.admin-sidebar-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 499;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.admin-sidebar-backdrop.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Mobile menu toggle — visible only on mobile, lives in toolbars */
|
||||
.admin-mobile-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--fg-dim);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.admin-mobile-toggle::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
box-shadow: 0 4px 0 currentColor, 0 8px 0 currentColor;
|
||||
}
|
||||
.admin-mobile-toggle:hover { color: var(--fg); }
|
||||
@media (max-width: 700px) {
|
||||
.admin-mobile-toggle { display: flex; }
|
||||
}
|
||||
|
||||
.admin-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -900,8 +1009,30 @@
|
||||
.watch-active { color: var(--green); font-weight: 500; }
|
||||
.watch-completed { color: var(--accent); }
|
||||
|
||||
/* Wide modal variant for schedule forms */
|
||||
.admin-modal-wide { width: 480px; }
|
||||
/* Two-column form layout for wide modals */
|
||||
.modal-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
.modal-col > label:first-child,
|
||||
.modal-col > .modal-col-heading + label { margin-top: 0; }
|
||||
.modal-col-heading {
|
||||
font-family: var(--font-display);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--accent);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.modal-columns > .modal-col:first-child {
|
||||
border-right: 1px solid var(--border);
|
||||
padding-right: 12px;
|
||||
}
|
||||
.modal-columns > .modal-col:last-child {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* Checkbox labels inside admin modals */
|
||||
.admin-modal label.admin-checkbox {
|
||||
@@ -930,16 +1061,32 @@
|
||||
padding: 32px;
|
||||
width: 380px;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.5), 0 0 80px -20px var(--accent-dim);
|
||||
position: relative;
|
||||
}
|
||||
.admin-modal.admin-modal-wide { width: 820px; }
|
||||
@media (max-width: 700px) {
|
||||
.admin-modal.admin-modal-wide { width: auto; }
|
||||
.modal-columns { grid-template-columns: 1fr; gap: 20px 0; }
|
||||
.modal-columns > .modal-col:first-child {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.modal-columns > .modal-col:last-child { padding-left: 0; }
|
||||
}
|
||||
.admin-modal::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px; left: 20%; right: 20%;
|
||||
top: 0; left: 20%; right: 20%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||
border-radius: 1px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.admin-modal h2 {
|
||||
font-family: var(--font-display);
|
||||
@@ -1030,7 +1177,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 500;
|
||||
z-index: 600;
|
||||
}
|
||||
|
||||
/* Token display (show-once) */
|
||||
@@ -1076,13 +1223,27 @@
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Admin tabs — horizontal scroll for 10+ tabs
|
||||
Admin sidebar — mobile off-canvas drawer
|
||||
========================================================================== */
|
||||
.admin-tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
flex-wrap: nowrap;
|
||||
scrollbar-width: thin;
|
||||
@media (max-width: 700px) {
|
||||
.admin-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
width: 220px;
|
||||
z-index: 500;
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-strong);
|
||||
border-right: none;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.25s ease;
|
||||
padding-top: 48px;
|
||||
}
|
||||
.admin-sidebar.open { transform: translateX(0); }
|
||||
.admin-sidebar.collapsed { transform: translateX(100%); width: 220px; }
|
||||
.admin-content { padding-right: 0; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -1349,7 +1510,9 @@
|
||||
.node-link, .dash-cell-node, .pagination button { transition: none; }
|
||||
.dash-row.has-link::after, .node-group-header::before { transition: none; }
|
||||
#new-ws-box select, #new-ws-box input, #new-ws-buttons button { transition: none; }
|
||||
.admin-tab, .admin-row, .admin-btn-danger, .admin-btn-action { transition: none; }
|
||||
.admin-nav, .admin-row, .admin-btn-danger, .admin-btn-action { transition: none; }
|
||||
.admin-sidebar, .admin-sidebar-backdrop { transition: none; }
|
||||
#view-admin { animation: none; }
|
||||
.admin-action-btn, .modal-cancel, .modal-submit { transition: none; }
|
||||
.admin-modal input, .admin-modal select { transition: none; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user