PageSourceSearch

https://smrtbld.ai/js/pages/system-admin-page.js

js smrtbld.ai collected 2026-10-05 15:07:47 UTC 6,963 bytes, 184 lines download raw bytes

1/**
2 * System Admin Page - Orchestrator
3 * Manages left-panel navigation and lazy-loads tab modules on demand.
4 * Mirrors the settings.js pattern exactly.
5 */
6
7import { createTemplateLoader, renderTo } from '../utils/templates.js';
8import { offlineAwareErrorDescription } from '../utils/async.js';
9import { auth } from '../auth.js';
10
11// ========================================
12// MODULE STATE
13// ========================================
14
15const tabs = {
16    'project-tasks': () => import('./system-admin/project-tasks.js'),
17    'doc-control-tasks': () => import('./system-admin/doc-control-tasks.js'),
18    'precon-tasks': () => import('./system-admin/precon-tasks.js'),
19    'tender-tasks': () => import('./system-admin/tender-tasks.js'),
20    'tender-package': () => import('./system-admin/tender-package.js'),
21    'access-pages': () => import('./system-admin/access-pages.js'),
22    'maintenance': () => import('./system-admin/maintenance.js')
23};
24
25let currentModule = null;
26let currentTabId = null;
27const ensureTemplates = createTemplateLoader('system-admin/sub-templates');
28let _navigating = false;
29// MCF-0710: the failure pane's retry is the page's own listener, not a module's, so it needs a
30// page-lifetime signal to be unbound by destroySystemAdminPage().
31let pageAbort = null;
32let switchGeneration = 0;
33let pageGeneration = 0;
34
35// ========================================
36// PUBLIC API
37// ========================================
38
39/**
40 * Initialize the system admin page and load the requested tab.
41 * @param {string|null} tabId - Tab to activate (defaults to 'project-tasks')
42 */
43export async function initSystemAdminPage(tabId = null) {
44    // Admin guard — redirect non-A users
45    const userType = auth.getUser()?.userType;
46    if (userType !== 'A') {
47        page.redirect('/');
48        return;
49    }
50
51    // ensureTemplates() fetches on a cold load, so teardown CAN land in this gap (card law 20).
52    const gen = ++pageGeneration;
53    await ensureTemplates();
54    if (gen !== pageGeneration) return;   // destroyed or re-mounted while the templates loaded
55
56    pageAbort = new AbortController();
57
58    const nav = document.getElementById('sysadmin-nav');
59    const initialTab = (tabId && tabs[tabId]) ? tabId : 'project-tasks';
60
61    // Bind nav click events
62    nav?.addEventListener('click', (e) => {
63        const btn = e.target.closest('.settings-nav-btn');
64        if (!btn) return;
65
66        const targetId = btn.dataset.tab;
67        if (targetId && targetId !== currentTabId) {
68            switchTab(targetId);
69        }
70    }, { signal: pageAbort.signal });   // MCF-0732: the page-lifetime owner created two lines up
71
72    // Load initial tab (suppress router re-entry)
73    _navigating = true;
74    try {
75        switchTab(initialTab);
76    } finally {
77        _navigating = false;
78    }
79}
80
81// ========================================
82// TAB SWITCHING
83// ========================================
84
85async function switchTab(tabId) {
86    if (tabId === currentTabId) return;
87
88    // Two fast clicks both get here (currentTabId is only assigned after the import resolves), so
89    // every publication below an await belongs to the newest switch or to nobody (card law 20).
90    const gen = ++switchGeneration;
91
92    // Update nav active state
93    const nav = document.getElementById('sysadmin-nav');
94    nav?.querySelectorAll('.settings-nav-btn').forEach(btn => {
95        btn.classList.toggle('active', btn.dataset.tab === tabId);
96    });
97
98    // Show loading in content area. Both hops of this chain ride the shared renderer: a retry press
99    // lands on the pane below, and an unwired hop between press and repaint drops focus to <body>.
100    const content = document.getElementById('sysadmin-content');
101    if (content) {
102        await renderTo(content, '#tpl-system-admin-tab-loading', {}, undefined, { restoreFocus: true });
103    }
104
105    // Cleanup previous module
106    if (currentModule?.destroy) {
107        currentModule.destroy();
108    }
109
110    try {
111        if (!tabs[tabId]) throw new Error(`Unknown tab: ${tabId}`);
112
113        // Update URL via router (unless we're already navigating from router)
114        const newUrl = `/system-admin/${tabId}`;
115        if (!_navigating) { page.show(newUrl); }
116
117        // Lazy-load and init module
118        const module = await tabs[tabId]();
119        if (gen !== switchGeneration) return;   // a newer switch overtook this one
120        currentModule = module;
121        currentTabId = tabId;
122
123        await module.init();
124
125    } catch (error) {
126        if (error.name === 'AbortError') return;
127        if (gen !== switchGeneration) return;   // a newer switch owns the content region now
128        console.error(`[SystemAdmin] Failed to load tab ${tabId}:`, error);
129
130        // MCF-0712(a): the failed tab already owns currentTabId, so its own nav button and the
131        // pane's retry would both no-op on the guard at the top of this function. Release it —
132        // and tear the half-initialised module down, since nulling the handle is the last chance.
133        currentModule?.destroy?.();
134        currentModule = null;
135        currentTabId = null;
136
137        // A failed lazy import is this pane's likeliest cause and its message carries an absolute
138        // URL, which is not user copy (DESIGN_BIBLE §Error State: no raw endpoint in the slot).
139        // The copy commits to no cause: a stale SW precache, a rotated chunk or a wrong MIME type
140        // all land here with a perfectly healthy connection.
141        const message = /dynamically imported module/i.test(error.message)
142            ? "Couldn't load this tab's code. Try again, or reload the page."
143            : offlineAwareErrorDescription(error);
144
145        if (content) {
146            await renderTo(content, '#tpl-system-admin-tab-error', { message }, undefined, { restoreFocus: true });
147            content.querySelector('#btn-sysadmin-tab-retry')?.addEventListener('click', async () => {
148                try {
149                    await switchTab(tabId);
150                } catch (err) {
151                    console.error(`[SystemAdmin] Retry of tab ${tabId} failed:`, err);
152                }
153            }, { once: true, signal: pageAbort?.signal });
154        }
155    }
156}
157
158/**
159 * Cleanup on page transition
160 */
161export function destroySystemAdminPage() {
162    pageGeneration++;      // any init still awaiting its templates must not publish into a dead page
163    switchGeneration++;    // ...and an in-flight switchTab must not bind a retry to a nulled signal
164    if (currentModule?.destroy) {
165        currentModule.destroy();
166    }
167    currentModule = null;
168    currentTabId = null;
169    _navigating = false;
170    pageAbort?.abort();
171    pageAbort = null;
172}
173
174export function navigateSystemAdminPage(tabId = null) {
175    _navigating = true;
176    try {
177        const target = (tabId && tabs[tabId]) ? tabId : 'project-tasks';
178        if (target !== currentTabId) {
179            switchTab(target);
180        }
181    } finally {
182        _navigating = false;
183    }
184}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.