librelad 98d950ba44 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>
2026-05-30 00:46:57 +01:00

63 lines
3.4 KiB
JavaScript

// 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,
};
},
};
})();