feat(ui): L-shell step 3a — Admin pane + rail Manage groups

Admin becomes a singleton pane and the rail's Manage section becomes its
navigation; the in-pane sidebar is retired.

- shell.js registers an `admin` pane type that adopts #view-admin (the 18
  tabpanels) on first open; the dashboard pane keeps #main.
- admin.js: new ADMIN_IA seam (window.TS_ADMIN) — the group→tab map, a shared
  adminTabAllowed() gate (mirrors the legacy showAdmin permission gate, incl.
  the ungated node list), an active-tab subscription, and openTab. showAdmin is
  now a thin delegator (openPane('admin') + switchAdminTab); the in-#main view
  toggle, breadcrumb write, history push, and mobile-hamburger injection go.
- rail.js: mountManage() builds the six collapsible .grp groups from the seam,
  permission-filtered, routing a row click through openTab — never touching
  admin DOM.
- app.js: home/drill re-focus the Dashboard pane instead of blanking the moved
  #view-admin.
- shell.css: the .grp vocabulary + admin-pane layout (in-pane sidebar hidden,
  #view-admin fills the pane).

The legacy #admin-sidebar is hidden via CSS pending its deletion in 3b; this is
the additive, independently-runnable half. Verified with a headless-Chrome
harness driving the real shell.js + rail.js over a stubbed seam, plus the
test_shell_js.py guards (19 passing).
This commit is contained in:
Patrick Buckley
2026-06-05 16:31:16 -07:00
parent 98b4e08d3f
commit 9c4ec4855d
6 changed files with 388 additions and 168 deletions
+41
View File
@@ -25,6 +25,7 @@ _SHELL_JS = _SHARED / "shell.js"
_PANE_JS = _SHARED / "pane.js"
_CONSOLE_INDEX = _ROOT / "turnstone/console/static/index.html"
_CONSOLE_APP = _ROOT / "turnstone/console/static/app.js"
_CONSOLE_ADMIN = _ROOT / "turnstone/console/static/admin.js"
_RAIL_JS = _SHARED / "rail.js"
_ESM_BUNDLES = [_SHELL_JS, _PANE_JS, _RAIL_JS]
@@ -178,3 +179,43 @@ def test_console_launcher_routes_by_persona() -> None:
"the active-coordinators table must be removed (the rail covers it)"
)
assert 'id="launcher-personas"' in index, "the persona toggle must be in the launcher panel"
def test_step3_admin_pane_registered_and_manage_mounted() -> None:
"""Step 3: the shell registers the singleton Admin pane (which adopts
#view-admin) and mounts the rail's Manage groups from the admin IA seam."""
body = _SHELL_JS.read_text(encoding="utf-8")
assert 'registerType("admin"' in body, "shell must register the admin pane type"
assert 'getElementById("view-admin")' in body, "the admin pane must adopt #view-admin"
assert "mountManage(" in body, "shell must mount the rail Manage groups"
assert "mountManage" in body and 'from "./rail.js"' in body, (
"shell must import mountManage from rail.js"
)
def test_step3_rail_manage_builds_from_admin_seam() -> None:
"""rail.js builds the Manage groups from the TS_ADMIN seam — perm-filtered,
collapsible .grp vocabulary, programmatic DOM — and routes a row click
through the seam's openTab rather than reaching into admin DOM."""
body = _RAIL_JS.read_text(encoding="utf-8")
assert "export function mountManage" in body, "rail must export mountManage"
assert "window.TS_ADMIN" in body, "Manage must read the admin IA seam"
assert "isTabAllowed" in body, "Manage must permission-filter its tabs"
assert "openTab" in body, "a Manage row click must route through the seam's openTab"
assert '"grp"' in body and '"grp-items"' in body, "Manage reuses the .grp vocabulary"
# collapse + active state ride a class + aria, never colour alone
assert "aria-expanded" in body, "collapsible group heads must expose aria-expanded"
def test_step3_admin_seam_and_thin_show_admin() -> None:
"""admin.js exposes the TS_ADMIN seam (IA + shared perm gate + active-tab +
openTab) and showAdmin is now a thin delegator that opens the singleton
Admin pane — the legacy in-#main view toggle + history push are gone."""
body = _CONSOLE_ADMIN.read_text(encoding="utf-8")
assert "const ADMIN_IA = [" in body, "admin must define the IA data"
assert "window.TS_ADMIN.ia = ADMIN_IA" in body, "admin must expose the IA seam"
assert "window.TS_ADMIN.isTabAllowed" in body and "window.TS_ADMIN.openTab" in body
assert "function adminTabAllowed(tab)" in body, "the shared permission gate must exist"
assert 'pm.openPane("admin")' in body, "showAdmin must open the singleton Admin pane"
for gone in ('currentView = "admin"', 'history.pushState({ view: "admin" }'):
assert gone not in body, f"legacy admin view-model bit {gone!r} must be gone"
+150 -159
View File
@@ -32,152 +32,156 @@ const ALIAS_SETTING_KEYS = [
const INHERIT_EMPTY_LABEL_KEYS = ["model.task_effort"];
// ---------------------------------------------------------------------------
// View switching (called from app.js showHome/drillDown pattern)
// Admin information architecture — the single source of truth for the rail's
// Manage groups (rail.js builds from this) AND in-pane tab activation. Each
// tab carries the permission scope that gates it, so the rail can filter
// without reaching into admin internals. `perm: null` = ungated (always
// shown — mirrors the legacy gate, which left node-metadata uncovered).
// ---------------------------------------------------------------------------
const ADMIN_IA = [
{
group: "Identity",
tabs: [
{ tab: "users", label: "Users", perm: "admin.users" },
{ tab: "tokens", label: "API Tokens", perm: "admin.users" },
{ tab: "channels", label: "Channels", perm: "admin.users" },
],
},
{
group: "Automation",
tabs: [
{ tab: "schedules", label: "Schedules", perm: "admin.schedules" },
{ tab: "watches", label: "Watches", perm: "admin.watches" },
],
},
{
group: "Governance",
tabs: [
{ tab: "roles", label: "Roles", perm: "admin.roles" },
{ tab: "policies", label: "Policies", perm: "admin.policies" },
{
tab: "prompt-policies",
label: "Prompts",
perm: "admin.prompt_policies",
},
{ tab: "judge", label: "Judge", perm: "admin.judge" },
],
},
{
group: "Extensions",
tabs: [
{ tab: "skills", label: "Skills", perm: "admin.skills" },
{ tab: "mcp", label: "MCP Servers", perm: "admin.mcp" },
],
},
{
group: "Observe",
tabs: [
{ tab: "usage", label: "Usage", perm: "admin.usage" },
{ tab: "audit", label: "Audit", perm: "admin.audit" },
{ tab: "memories", label: "Memories", perm: "admin.memories" },
],
},
{
group: "System",
tabs: [
{ tab: "models", label: "Models", perm: "admin.models" },
{ tab: "node-metadata", label: "Nodes", perm: null },
{ tab: "settings", label: "Settings", perm: "admin.settings" },
{ tab: "tls", label: "TLS", perm: "admin.settings" },
],
},
];
function _adminTabMeta(tab) {
for (const grp of ADMIN_IA) {
for (const t of grp.tabs) if (t.tab === tab) return t;
}
return null;
}
// Mirror the legacy showAdmin gate exactly: only gate when a permission string
// is present (unknown perms → show everything); ungated tabs (perm: null) and
// tabs whose scope the user holds are allowed.
function adminTabAllowed(tab) {
const raw = sessionStorage.getItem("turnstone_permissions");
if (!raw) return true;
const meta = _adminTabMeta(tab);
const needed = meta && meta.perm;
if (!needed) return true;
return raw.split(",").indexOf(needed) >= 0;
}
function _firstAllowedAdminTab() {
for (const grp of ADMIN_IA) {
for (const t of grp.tabs) if (adminTabAllowed(t.tab)) return t.tab;
}
return null;
}
// No-permissions empty state in the admin content host (ported from the legacy
// sidebar gate — still reachable when a user holds no admin scope at all).
function _showAdminNoPermissions() {
const panels = document.querySelectorAll(".admin-panel");
for (let j = 0; j < panels.length; j++) panels[j].style.display = "none";
let empty = document.getElementById("admin-no-permissions");
if (!empty) {
empty = document.createElement("div");
empty.id = "admin-no-permissions";
empty.className = "dashboard-empty";
empty.textContent = "You do not have permissions to view any admin tabs.";
const content = document.getElementById("admin-content");
if (content) content.appendChild(empty);
}
empty.style.display = "";
}
// The rail's Manage groups subscribe here to mirror the active admin tab.
const _adminTabSubs = [];
function _notifyAdminTab(tab) {
for (const cb of _adminTabSubs) {
try {
cb(tab);
} catch (e) {
/* a faulty subscriber must not break tab switching */
}
}
}
// Seam consumed by rail.js (the Manage section): the admin IA, its permission
// gate, the active-tab subscription, the current tab, and the open-on-tab entry
// point (a row click opens/focuses the Admin pane on that tab).
window.TS_ADMIN = window.TS_ADMIN || {};
window.TS_ADMIN.ia = ADMIN_IA;
window.TS_ADMIN.isTabAllowed = adminTabAllowed;
window.TS_ADMIN.onTabChange = function (cb) {
if (typeof cb === "function") _adminTabSubs.push(cb);
};
window.TS_ADMIN.getActiveTab = function () {
return _adminTab;
};
window.TS_ADMIN.openTab = function (tab) {
showAdmin(tab);
};
// ---------------------------------------------------------------------------
// View switching (called from app.js showHome/drillDown pattern + the rail)
// ---------------------------------------------------------------------------
function showAdmin() {
/* global currentView, showHome */
// Toggle: if already in admin view, go back to the home landing
if (currentView === "admin") {
const adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
adminBtn.setAttribute("aria-expanded", "false");
}
showHome();
return;
}
function showAdmin(tab) {
// The admin surface is a singleton pane now — the L-shell PaneManager owns
// show/hide and the rail's Manage groups are its navigation. Open/focus the
// pane (its onMount adopts #view-admin), then activate a tab the user may see.
const pm = window.TS_SHELL && window.TS_SHELL.panes;
if (pm && pm.hasType("admin")) pm.openPane("admin");
currentView = "admin";
const homeView = document.getElementById("view-home");
if (homeView) homeView.style.display = "none";
document.getElementById("view-filtered").style.display = "none";
document.getElementById("view-admin").style.display = "";
document.getElementById("breadcrumb").style.display = "";
document.getElementById("breadcrumb-label").textContent = "Admin";
document.getElementById("main").scrollTop = 0;
// Highlight admin button as active
const adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.add("active");
adminBtn.setAttribute("aria-expanded", "true");
}
history.pushState({ view: "admin" }, "");
// Permission gating: hide nav items the user cannot access
const perms = sessionStorage.getItem("turnstone_permissions") || "";
const tabPerms = {
users: "admin.users",
tokens: "admin.users",
channels: "admin.users",
schedules: "admin.schedules",
watches: "admin.watches",
roles: "admin.roles",
policies: "admin.policies",
"prompt-policies": "admin.prompt_policies",
judge: "admin.judge",
skills: "admin.skills",
usage: "admin.usage",
audit: "admin.audit",
memories: "admin.memories",
settings: "admin.settings",
tls: "admin.settings",
mcp: "admin.mcp",
models: "admin.models",
};
if (perms) {
const permSet = perms.split(",");
const navItems = document.querySelectorAll(".admin-nav");
for (let i = 0; i < navItems.length; i++) {
const tabName = navItems[i].getAttribute("data-tab");
const needed = tabPerms[tabName];
if (needed && permSet.indexOf(needed) < 0) {
navItems[i].style.display = "none";
} else {
navItems[i].style.display = "";
}
}
}
// Hide groups where all children are permission-hidden
const groups = document.querySelectorAll(".admin-sidebar-group");
for (let g = 0; g < groups.length; g++) {
const 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
const sidebar = document.getElementById("admin-sidebar");
// Inject close header for mobile drawer (once)
if (!document.getElementById("admin-sidebar-close")) {
const closeHeader = document.createElement("div");
closeHeader.id = "admin-sidebar-close";
closeHeader.className = "admin-sidebar-close";
const label = document.createElement("span");
label.textContent = "Navigation";
const closeBtn = document.createElement("button");
closeBtn.setAttribute("aria-label", "Close navigation");
closeBtn.textContent = "\u00d7";
closeBtn.addEventListener("click", function () {
if (_mobileSidebarOpen) {
_toggleMobileSidebar();
const mt = document.getElementById("admin-mobile-toggle");
if (mt) mt.focus();
}
});
closeHeader.appendChild(label);
closeHeader.appendChild(closeBtn);
sidebar.insertBefore(closeHeader, sidebar.firstChild);
}
if (window.innerWidth <= 700) {
_mobileSidebarOpen = false;
sidebar.classList.add("collapsed");
sidebar.classList.remove("open");
sidebar.setAttribute("aria-hidden", "true");
sidebar.setAttribute("inert", "");
} else {
sidebar.removeAttribute("aria-hidden");
sidebar.removeAttribute("inert");
}
// Mobile backdrop listener (idempotent)
const backdrop = document.getElementById("admin-sidebar-backdrop");
if (backdrop && !backdrop._listenerAttached) {
backdrop.addEventListener("click", function () {
if (_mobileSidebarOpen) {
_toggleMobileSidebar();
const mt = document.getElementById("admin-mobile-toggle");
if (mt) mt.focus();
}
});
backdrop._listenerAttached = true;
}
// Switch to the first visible nav item
const 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
const panels = document.querySelectorAll(".admin-panel");
for (let j = 0; j < panels.length; j++) panels[j].style.display = "none";
let empty = document.getElementById("admin-no-permissions");
if (!empty) {
empty = document.createElement("div");
empty.id = "admin-no-permissions";
empty.className = "dashboard-empty";
empty.textContent = "You do not have permissions to view any admin tabs.";
document.getElementById("admin-content").appendChild(empty);
}
empty.style.display = "";
}
const target =
tab ||
(_adminTab && adminTabAllowed(_adminTab)
? _adminTab
: _firstAllowedAdminTab());
if (target) switchAdminTab(target);
else _showAdminNoPermissions();
}
function _injectMobileToggle(tab) {
@@ -297,22 +301,9 @@ function switchAdminTab(tab) {
const 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();
// Move focus to the newly active panel instead of leaving it in the inert sidebar
const panel = document.getElementById("admin-" + tab);
const focusTarget =
panel &&
panel.querySelector("h2, .section-header, button:not([disabled])");
if (focusTarget) {
focusTarget.setAttribute("tabindex", "-1");
focusTarget.focus();
}
}
// Mirror the active tab into the rail's Manage groups (the in-pane sidebar
// that used to carry the active state is retired — the rail navigates now).
_notifyAdminTab(tab);
}
// ---------------------------------------------------------------------------
+12 -6
View File
@@ -427,8 +427,10 @@ function showHome() {
currentView = "home";
currentFilter = { state: null, node: null, page: 1, per_page: 50 };
_setLandingView("home");
const adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
// #view-admin is its own pane now; re-focus the Dashboard pane (home/filtered
// sub-views live inside it) instead of toggling the old in-#main admin view.
const pm = window.TS_SHELL && window.TS_SHELL.panes;
if (pm) pm.openPane("dashboard");
const adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
@@ -472,8 +474,10 @@ function drillDownByState(state) {
currentView = "filtered";
currentFilter = { state: state, node: null, page: 1, per_page: 50 };
_setLandingView("filtered");
const adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
// #view-admin is its own pane now; re-focus the Dashboard pane (home/filtered
// sub-views live inside it) instead of toggling the old in-#main admin view.
const pm = window.TS_SHELL && window.TS_SHELL.panes;
if (pm) pm.openPane("dashboard");
const adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
@@ -497,8 +501,10 @@ function drillDownByNode(nodeId) {
currentView = "filtered";
currentFilter = { state: null, node: nodeId, page: 1, per_page: 50 };
_setLandingView("filtered");
const adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
// #view-admin is its own pane now; re-focus the Dashboard pane (home/filtered
// sub-views live inside it) instead of toggling the old in-#main admin view.
const pm = window.TS_SHELL && window.TS_SHELL.panes;
if (pm) pm.openPane("dashboard");
const adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
+86
View File
@@ -267,3 +267,89 @@ export function mountRail(sections, caps) {
if (TS.onRender) TS.onRender(render);
render(); // initial paint (empty until the first snapshot arrives)
}
// ---- Manage section (admin IA → collapsible discovery groups) --------------
/**
* Build the rail's Manage groups from the admin IA seam (admin.js exposes
* `window.TS_ADMIN`). Each group is a collapsible `.grp` whose head toggles
* its `.grp-items`; items are the admin tabs the user is permitted to see. A
* row click opens/focuses the singleton Admin pane on that tab via the seam's
* `openTab`. The IA is static, so this builds once; only the active-row marker
* re-renders, driven by the admin tab-change subscription (single writer).
*
* House style: programmatic DOM, NO innerHTML; reuses the mock's
* `.grp`/`.grp-head`/`.grp-items` vocabulary (shell.css).
*/
export function mountManage(root) {
if (!root) return;
const TS = window.TS_ADMIN || {};
const ia = TS.ia || [];
const allowed = TS.isTabAllowed || (() => true);
root.replaceChildren();
const rowByTab = new Map(); // tab -> its row <button>, for active-state sync
ia.forEach((group, gi) => {
const tabs = group.tabs.filter((t) => allowed(t.tab));
if (!tabs.length) return; // every tab in the group is gated away → drop it
const grp = document.createElement("div");
grp.className = "grp";
if (gi === 0) grp.classList.add("open"); // first group expanded (mock)
const itemsId = "manage-grp-" + group.group.toLowerCase();
const head = document.createElement("button");
head.type = "button";
head.className = "grp-head";
head.setAttribute("aria-expanded", gi === 0 ? "true" : "false");
head.setAttribute("aria-controls", itemsId);
const chev = document.createElement("span");
chev.className = "chev";
chev.setAttribute("aria-hidden", "true");
chev.textContent = gi === 0 ? "▾" : "▸";
const name = document.createElement("span");
name.className = "gname";
name.textContent = group.group;
const count = document.createElement("span");
count.className = "gcount";
count.textContent = String(tabs.length);
head.append(chev, name, count);
const items = document.createElement("div");
items.className = "grp-items";
items.id = itemsId;
for (const t of tabs) {
const row = document.createElement("button");
row.type = "button";
row.className = "row";
row.dataset.tab = t.tab;
const nm = document.createElement("span");
nm.className = "nm";
nm.textContent = t.label;
row.append(nm);
row.addEventListener("click", () => {
if (TS.openTab) TS.openTab(t.tab);
});
rowByTab.set(t.tab, row);
items.append(row);
}
head.addEventListener("click", () => {
const open = grp.classList.toggle("open");
head.setAttribute("aria-expanded", open ? "true" : "false");
chev.textContent = open ? "▾" : "▸";
});
grp.append(head, items);
root.append(grp);
});
// Single writer for the Manage active-row: the row for the current admin tab
// carries `.active`. admin.js notifies on every switchAdminTab; nothing is
// marked until the user actually navigates the admin pane.
function markActive(tab) {
for (const [t, row] of rowByTab) row.classList.toggle("active", t === tab);
}
if (TS.onTabChange) TS.onTabChange(markActive);
}
+75
View File
@@ -560,8 +560,83 @@
.cpill:focus-visible,
.node-row:focus-visible,
.row:focus-visible,
.grp-head:focus-visible,
.persona-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
border-radius: var(--r-sm);
}
/* ===== Rail Manage section the admin IA as collapsible discovery groups
(rail.js mountManage). Reuses the mock's .grp / .grp-head / .grp-items
vocabulary; heads + rows are <button>s so they're keyboard-operable, and
collapse state rides the chevron + aria-expanded (no colour-only cue). ===== */
.grp {
margin-top: 1px;
}
.grp-head {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 9px;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--ink-3);
font-size: 13px;
width: 100%;
background: none;
border: 0;
text-align: left;
font-family: var(--font-ui);
}
.grp-head:hover {
background: var(--panel-2);
}
.grp-head .chev {
width: 12px;
color: var(--ink-4);
font-size: 10px;
flex: none;
}
.grp-head .gname {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.grp-head .gcount {
color: var(--ink-4);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.grp:not(.open) .grp-items {
display: none;
}
.grp-items {
padding-left: 20px;
}
.grp-items .row {
color: var(--ink-3);
font-size: 12.5px;
padding: 5px 9px;
}
/* the current admin tab a subtle single marker (the rail mirrors which tab
the Admin pane is showing; distinct from the amber `.open` of live sessions) */
.grp-items .row.active {
color: var(--ink);
background: color-mix(in srgb, var(--accent) 8%, var(--panel-2));
}
/* ===== Admin pane adopts #view-admin (the 18 tabpanels). The in-pane
sidebar + its mobile backdrop are retired (the rail's Manage groups are the
navigation now), so #view-admin fills the pane body and content is full-width. */
.pane-body > #view-admin {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.pane-body .admin-sidebar,
.pane-body .admin-sidebar-backdrop {
display: none;
}
+24 -3
View File
@@ -20,7 +20,7 @@
========================================================================== */
import { PaneManager, ShellPane } from "./pane.js";
import { mountRail } from "./rail.js";
import { mountRail, mountManage } from "./rail.js";
function make(tag, className, text) {
const node = document.createElement(tag);
@@ -56,7 +56,7 @@ function buildShell(caps) {
scroll.append(connSlot);
// IA sections — each a label + a render target. Cluster is capability-gated
// (hidden on a single-node standalone deployment); rail.js fills Cluster +
// Workspaces from Tier-1. Manage stays a stub label until step 3.
// Workspaces from Tier-1 and the Manage groups from the admin IA (step 3).
function section(title) {
scroll.append(make("div", "sec-label", title));
const body = make("div", "rail-section");
@@ -65,7 +65,7 @@ function buildShell(caps) {
}
const clusterSec = caps.cluster ? section("Cluster") : null;
const workspacesSec = section("Workspaces");
scroll.append(make("div", "sec-label", "Manage"));
const manageSec = section("Manage");
rail.append(scroll);
const foot = make("div", "rail-foot");
@@ -91,6 +91,7 @@ function buildShell(caps) {
panes,
clusterSec,
workspacesSec,
manageSec,
};
}
@@ -102,6 +103,7 @@ function mountShell() {
const statusBarEl = document.getElementById("status-bar");
const breadcrumbEl = document.getElementById("breadcrumb");
const mainEl = document.getElementById("main");
const viewAdminEl = document.getElementById("view-admin");
const shell = buildShell(caps);
// Insert the shell as the first body child so it owns the viewport; portals
@@ -153,6 +155,21 @@ function mountShell() {
return pane;
});
// Admin pane (step 3): a singleton that ADOPTS #view-admin (the 18 admin
// tabpanels). The rail's Manage groups are its navigation — the in-pane
// sidebar is retired. Lazy-mounts on first openPane('admin'); by then the
// dashboard pane already hosts #main, so #view-admin moves out of it here.
pm.registerType("admin", () => {
const pane = new ShellPane({ type: "admin", title: "Admin", glyph: "⚙" });
pane.onMount = function () {
if (viewAdminEl) {
viewAdminEl.style.display = ""; // clear the inline display:none guard
this.bodyEl.append(viewAdminEl);
}
};
return pane;
});
// Restore the persisted working set, else open the default Dashboard pane.
if (!pm.rehydrate()) pm.openPane("dashboard");
@@ -169,6 +186,10 @@ function mountShell() {
caps,
);
// Manage section — the admin IA as collapsible discovery groups; a row click
// routes through the TS_ADMIN seam (opens/focuses the singleton Admin pane).
mountManage(shell.manageSec);
// Hand off to the legacy boot (login + Tier-1 stream) now that the shell and
// its status DOM exist.
if (window.TS_APP && typeof window.TS_APP.boot === "function") {