feat(webui): phase 2 — DI service container (ctx.services)
Introduces kernel/services.js (window.LP.services): an additive, typed,
LAZY view onto the existing cross-cutting singletons — tasks{bus,refresh,
route}, live, auth, data, notify, theme, modal, router. It constructs
nothing (pure getters onto the live globals), so there's no double-init and
the globals stay authoritative. MountContext now injects it as ctx.services.
Slot names/globals were verified against the real code (workflow map): the
design doc's §4 list was wrong in several places — no window.taskManager
(client slot dropped), tasks.route lives on tasksManager.router, auth has no
status(), DataLoader isn't a window prop (lexical fallback), modal/router are
split surfaces (grouped/bound objects).
Migrated the 4 cross-cutting refs in the feature modules onto ctx.services
(admin: router.adminCategoryFromPath + tasks.refresh; backup: tasks.refresh;
app-detail: router.appPath). Page-owned controllers stay feature-globals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
cd34a7671a
commit
98d950ba44
@ -14,7 +14,7 @@ LP.features.register({
|
|||||||
routes: ['/admin', '/admin*'],
|
routes: ['/admin', '/admin*'],
|
||||||
|
|
||||||
async mount(ctx) {
|
async mount(ctx) {
|
||||||
window.configCategory = window.adminCategoryFromPath(window.location.pathname);
|
window.configCategory = ctx.services.router.adminCategoryFromPath(window.location.pathname);
|
||||||
|
|
||||||
const html = await ctx.loadFragment('/html/config-content.html');
|
const html = await ctx.loadFragment('/html/config-content.html');
|
||||||
ctx.setContent(html, 'Admin');
|
ctx.setContent(html, 'Admin');
|
||||||
@ -30,7 +30,7 @@ LP.features.register({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async unmount() {
|
async unmount(ctx) {
|
||||||
// Release only this view's leaks; never destroy the configManager singleton.
|
// Release only this view's leaks; never destroy the configManager singleton.
|
||||||
// The sub-controllers renderConfig() spawns are re-created on each visit, so
|
// The sub-controllers renderConfig() spawns are re-created on each visit, so
|
||||||
// null them; AdminSystem additionally holds a live SSE sub + a 30s interval
|
// null them; AdminSystem additionally holds a live SSE sub + a 30s interval
|
||||||
@ -48,7 +48,7 @@ LP.features.register({
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
// Drop AdminOverview's task-refresh registration so a finished verify/update
|
// Drop AdminOverview's task-refresh registration so a finished verify/update
|
||||||
// task doesn't repaint a torn-down board.
|
// task doesn't repaint a torn-down board.
|
||||||
try { window.taskRefresh && window.taskRefresh.unregister && window.taskRefresh.unregister('admin-overview'); } catch (_) {}
|
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('admin-overview'); } catch (_) {}
|
||||||
window.adminOverview = null;
|
window.adminOverview = null;
|
||||||
window.adminSystem = null;
|
window.adminSystem = null;
|
||||||
window.sshPage = null;
|
window.sshPage = null;
|
||||||
|
|||||||
@ -36,7 +36,7 @@ LP.features.register({
|
|||||||
const tab = legacyTab === 'logs' ? 'tasks' : (legacyTab || 'config');
|
const tab = legacyTab === 'logs' ? 'tasks' : (legacyTab || 'config');
|
||||||
const sub = (tab === 'config') ? legacyConfig : null;
|
const sub = (tab === 'config') ? legacyConfig : null;
|
||||||
const taskId = url.searchParams.get('task');
|
const taskId = url.searchParams.get('task');
|
||||||
const canonical = window.appPath(appName, tab, sub, taskId);
|
const canonical = ctx.services.router.appPath(appName, tab, sub, taskId);
|
||||||
if (canonical !== url.pathname + url.search) {
|
if (canonical !== url.pathname + url.search) {
|
||||||
window.history.replaceState({ route: canonical }, '', canonical);
|
window.history.replaceState({ route: canonical }, '', canonical);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,13 +29,13 @@ LP.features.register({
|
|||||||
await window.backupPage.init();
|
await window.backupPage.init();
|
||||||
},
|
},
|
||||||
|
|
||||||
async unmount() {
|
async unmount(ctx) {
|
||||||
// Best-effort teardown. BackupPage self-guards stale work via
|
// Best-effort teardown. BackupPage self-guards stale work via
|
||||||
// (window.backupPage === this), so nulling the global neutralises any
|
// (window.backupPage === this), so nulling the global neutralises any
|
||||||
// pending task-refresh repaint; we also drop its coordinator registration.
|
// pending task-refresh repaint; we also drop its coordinator registration.
|
||||||
// A proper dispose() (removing the leaked document listeners) lands with
|
// A proper dispose() (removing the leaked document listeners) lands with
|
||||||
// the Phase 5 backup decomposition.
|
// the Phase 5 backup decomposition.
|
||||||
try { window.taskRefresh && window.taskRefresh.unregister && window.taskRefresh.unregister('backups'); } catch (_) {}
|
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('backups'); } catch (_) {}
|
||||||
window.backupPage = null;
|
window.backupPage = null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -108,6 +108,7 @@
|
|||||||
loads the page manifest; spa.js consults it for routing. See
|
loads the page manifest; spa.js consults it for routing. See
|
||||||
docs/frontend-modularization.md. -->
|
docs/frontend-modularization.md. -->
|
||||||
<script src="/kernel/feature-registry.js"></script>
|
<script src="/kernel/feature-registry.js"></script>
|
||||||
|
<script src="/kernel/services.js"></script>
|
||||||
<script src="/kernel/lifecycle.js"></script>
|
<script src="/kernel/lifecycle.js"></script>
|
||||||
<!-- Feature modules are NOT listed here: the kernel loads each feature's
|
<!-- Feature modules are NOT listed here: the kernel loads each feature's
|
||||||
self-registering index.js from the manifest (features/manifest.dev.json)
|
self-registering index.js from the manifest (features/manifest.dev.json)
|
||||||
|
|||||||
@ -16,6 +16,9 @@
|
|||||||
this.shell = shell; // the LibrePortalSPAClean instance
|
this.shell = shell; // the LibrePortalSPAClean instance
|
||||||
this.route = route || {};
|
this.route = route || {};
|
||||||
this.path = this.route.path || (location.pathname + location.search);
|
this.path = this.route.path || (location.pathname + location.search);
|
||||||
|
// Injected DI container — the cross-cutting services (tasks/live/auth/
|
||||||
|
// data/notify/theme/modal/router). Lazy getters onto the live singletons.
|
||||||
|
this.services = (window.LP && window.LP.services) || {};
|
||||||
this._ac = new AbortController(); // backs ctx.on() — abort kills all
|
this._ac = new AbortController(); // backs ctx.on() — abort kills all
|
||||||
this._unsubs = []; // backs ctx.sub() — SSE/bus/timer handles
|
this._unsubs = []; // backs ctx.sub() — SSE/bus/timer handles
|
||||||
}
|
}
|
||||||
|
|||||||
62
containers/libreportal/frontend/kernel/services.js
Normal file
62
containers/libreportal/frontend/kernel/services.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// kernel/services.js — the dependency-injection container (window.LP.services).
|
||||||
|
//
|
||||||
|
// An ADDITIVE, typed, lazy view onto the existing cross-cutting singletons. It
|
||||||
|
// constructs NOTHING: every member is a getter that returns the live global, so
|
||||||
|
// there is no double-instantiation and the globals stay authoritative. Feature
|
||||||
|
// modules receive it as ctx.services (kernel/lifecycle.js) and use it instead of
|
||||||
|
// reaching for window.* directly — the seam the rest of the de-globalisation
|
||||||
|
// builds on. Access only inside mount()/unmount() (post-boot), never at a
|
||||||
|
// feature's module top level, so the lazy getters always resolve.
|
||||||
|
//
|
||||||
|
// NOTE: page-owned controllers (appsManager, appTabbedManager, configManager,
|
||||||
|
// backupPage, adminSystem, …) are NOT services — they belong to their feature —
|
||||||
|
// so they are deliberately absent here. Slot names + globals were verified
|
||||||
|
// against the real code (the design doc's §4 list had several wrong names).
|
||||||
|
(function () {
|
||||||
|
const LP = (window.LP = window.LP || {});
|
||||||
|
|
||||||
|
LP.services = {
|
||||||
|
// Task system — the locked-down mutation front door + the live status bus.
|
||||||
|
tasks: {
|
||||||
|
get bus() { return window.taskEventBus; }, // the single SSE EventSource
|
||||||
|
get refresh() { return window.taskRefresh; }, // refresh coordinator (register/unregister)
|
||||||
|
// .routeAction(action, params) is the mutation path (mutations-via-tasks).
|
||||||
|
// Can be null if TasksManager construction failed — callers keep `?.`.
|
||||||
|
get route() { return window.tasksManager ? window.tasksManager.router : null; },
|
||||||
|
},
|
||||||
|
get live() { return window.LiveSystem; }, // 1 Hz system SSE: subscribe/pause/resume
|
||||||
|
get auth() { return window.authManager; }, // logout()/interceptFetch()/isAuthenticated
|
||||||
|
// DataLoader is a bare classic-script class (not a window property), so fall
|
||||||
|
// back to the lexical binding.
|
||||||
|
get data() { return (typeof DataLoader !== 'undefined') ? DataLoader : (window.DataLoader || null); },
|
||||||
|
// Reuse the codebase's own get-or-create net; never `new NotificationSystem()`
|
||||||
|
// here (would append a second toast container).
|
||||||
|
get notify() { return window.notificationSystem || (window.ensureNotificationSystem && window.ensureNotificationSystem()); },
|
||||||
|
get theme() { return window.ThemeRegistry; }, // theme discovery/list
|
||||||
|
// The eo-modal toolkit is six free functions, not one object — group them.
|
||||||
|
get modal() {
|
||||||
|
return {
|
||||||
|
open: window.openEoModal,
|
||||||
|
section: window.eoSection, badgeRow: window.eoBadgeRow,
|
||||||
|
urlList: window.eoUrlList, credList: window.eoCredList, empty: window.eoEmpty,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// The router surface is split: the spaClean instance (runtime methods, bound
|
||||||
|
// to preserve `this`) + the standalone URL helpers. Return a fresh delegating
|
||||||
|
// object — never Object.assign onto the live singleton.
|
||||||
|
get router() {
|
||||||
|
const spa = window.spaClean;
|
||||||
|
return {
|
||||||
|
navigate: (...a) => spa && spa.navigate(...a),
|
||||||
|
loadScript: (s) => spa && spa.loadScript(s),
|
||||||
|
fetchContent: (u) => spa && spa.fetchContent(u),
|
||||||
|
loadContent: (h, t) => spa && spa.loadContent(h, t),
|
||||||
|
navigateToRoute: window.navigateToRoute,
|
||||||
|
appPath: window.appPath,
|
||||||
|
appPartsFromPath: window.appPartsFromPath,
|
||||||
|
adminPath: window.adminPath,
|
||||||
|
adminCategoryFromPath: window.adminCategoryFromPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user