qdadm 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +2 -1
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/layout/AppLayout.vue +13 -1
  4. package/src/components/layout/Zone.vue +40 -23
  5. package/src/composables/index.js +1 -0
  6. package/src/composables/useAuth.js +43 -4
  7. package/src/composables/useCurrentEntity.js +44 -0
  8. package/src/composables/useFormPageBuilder.js +3 -3
  9. package/src/composables/useNavContext.js +24 -8
  10. package/src/debug/AuthCollector.js +254 -0
  11. package/src/debug/Collector.js +235 -0
  12. package/src/debug/DebugBridge.js +163 -0
  13. package/src/debug/DebugModule.js +215 -0
  14. package/src/debug/EntitiesCollector.js +376 -0
  15. package/src/debug/ErrorCollector.js +66 -0
  16. package/src/debug/LocalStorageAdapter.js +150 -0
  17. package/src/debug/SignalCollector.js +87 -0
  18. package/src/debug/ToastCollector.js +82 -0
  19. package/src/debug/ZonesCollector.js +300 -0
  20. package/src/debug/components/DebugBar.vue +1232 -0
  21. package/src/debug/components/ObjectTree.vue +194 -0
  22. package/src/debug/components/index.js +8 -0
  23. package/src/debug/components/panels/AuthPanel.vue +103 -0
  24. package/src/debug/components/panels/EntitiesPanel.vue +616 -0
  25. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  26. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  27. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  28. package/src/debug/components/panels/index.js +8 -0
  29. package/src/debug/index.js +31 -0
  30. package/src/entity/EntityManager.js +142 -20
  31. package/src/entity/storage/MockApiStorage.js +17 -1
  32. package/src/entity/storage/index.js +9 -2
  33. package/src/index.js +7 -0
  34. package/src/kernel/Kernel.js +436 -48
  35. package/src/kernel/KernelContext.js +385 -0
  36. package/src/kernel/Module.js +111 -0
  37. package/src/kernel/ModuleLoader.js +573 -0
  38. package/src/kernel/SignalBus.js +2 -7
  39. package/src/kernel/index.js +14 -0
  40. package/src/toast/ToastBridgeModule.js +70 -0
  41. package/src/toast/ToastListener.vue +47 -0
  42. package/src/toast/index.js +15 -0
  43. package/src/toast/useSignalToast.js +113 -0
@@ -37,7 +37,7 @@
37
37
  * ```
38
38
  */
39
39
 
40
- import { createApp } from 'vue'
40
+ import { createApp, h } from 'vue'
41
41
  import { createPinia } from 'pinia'
42
42
  import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
43
43
  import ToastService from 'primevue/toastservice'
@@ -45,7 +45,9 @@ import ConfirmationService from 'primevue/confirmationservice'
45
45
  import Tooltip from 'primevue/tooltip'
46
46
 
47
47
  import { createQdadm } from '../plugin.js'
48
- import { initModules, getRoutes, setSectionOrder, alterMenuSections } from '../module/moduleRegistry.js'
48
+ import { initModules, getRoutes, setSectionOrder, alterMenuSections, registry } from '../module/moduleRegistry.js'
49
+ import { createModuleLoader } from './ModuleLoader.js'
50
+ import { createKernelContext } from './KernelContext.js'
49
51
  import { Orchestrator } from '../orchestrator/Orchestrator.js'
50
52
  import { createSignalBus } from './SignalBus.js'
51
53
  import { createZoneRegistry } from '../zones/ZoneRegistry.js'
@@ -58,12 +60,18 @@ import { createDeferredRegistry } from '../deferred/DeferredRegistry.js'
58
60
  import { createEventRouter } from './EventRouter.js'
59
61
  import { createSSEBridge } from './SSEBridge.js'
60
62
 
63
+ // Debug imports are dynamic to enable tree-shaking in production
64
+ // When debugBar: false/undefined, no debug code is bundled
65
+ let DebugModule = null
66
+ let QdadmDebugBar = null
67
+
61
68
  export class Kernel {
62
69
  /**
63
70
  * @param {object} options
64
71
  * @param {object} options.root - Root Vue component
65
- * @param {object} options.modules - Result of import.meta.glob for module init files
72
+ * @param {object} options.modules - Result of import.meta.glob for module init files (legacy)
66
73
  * @param {object} options.modulesOptions - Options for initModules (e.g., { coreNavItems })
74
+ * @param {Array} options.moduleDefs - New-style module definitions (Module classes/objects/functions)
67
75
  * @param {string[]} options.sectionOrder - Navigation section order
68
76
  * @param {object} options.managers - Entity managers { name: config } - can be instances, strings, or config objects
69
77
  * @param {object} options.managerRegistry - Registry of manager classes from qdadm-gen { name: ManagerClass }
@@ -71,7 +79,9 @@ export class Kernel {
71
79
  * @param {function} options.managerResolver - Custom manager resolver (config, entityName, context) => Manager
72
80
  * @param {object} options.authAdapter - Auth adapter for login/logout (app-level authentication)
73
81
  * @param {object} options.entityAuthAdapter - Auth adapter for entity permissions (scope/silo checks)
74
- * @param {object} options.pages - Page components { login, layout }
82
+ * @param {object} options.pages - Page components { layout, shell? }
83
+ * @param {object} options.pages.layout - Main layout component (required)
84
+ * @param {object} options.pages.shell - Optional app shell (enables unified routing with zones everywhere)
75
85
  * @param {string} options.homeRoute - Route name for home redirect (or object { name, component })
76
86
  * @param {Array} options.coreRoutes - Additional routes as layout children (before module routes)
77
87
  * @param {string} options.basePath - Base path for router (e.g., '/dashboard/')
@@ -84,8 +94,31 @@ export class Kernel {
84
94
  * @param {boolean} options.warmup - Enable warmup at boot (default: true)
85
95
  * @param {object} options.eventRouter - EventRouter config { 'source:signal': ['target:signal', ...] }
86
96
  * @param {object} options.sse - SSEBridge config { url, reconnectDelay, signalPrefix, autoConnect, events }
97
+ * @param {object} options.debugBar - Debug bar config { module: DebugModule, component: QdadmDebugBar, ...options }
87
98
  */
88
99
  constructor(options) {
100
+ // Auto-inject DebugModule if debugBar.module is provided
101
+ // User must import DebugModule separately for tree-shaking
102
+ if (options.debugBar?.module) {
103
+ const DebugModuleClass = options.debugBar.module
104
+ const { module: _, component: __, ...debugModuleOptions } = options.debugBar
105
+ // Enable by default when using debugBar shorthand
106
+ if (debugModuleOptions.enabled === undefined) {
107
+ debugModuleOptions.enabled = true
108
+ }
109
+ // Mark as kernel-managed to prevent zone block registration
110
+ // (Kernel handles rendering via root wrapper)
111
+ debugModuleOptions._kernelManaged = true
112
+ const debugModule = new DebugModuleClass(debugModuleOptions)
113
+ options.moduleDefs = options.moduleDefs || []
114
+ options.moduleDefs.push(debugModule)
115
+ // Store component for root wrapper
116
+ QdadmDebugBar = options.debugBar.component
117
+ // Enable debug mode
118
+ if (!options.debug) {
119
+ options.debug = true
120
+ }
121
+ }
89
122
  this.options = options
90
123
  this.vueApp = null
91
124
  this.router = null
@@ -98,10 +131,21 @@ export class Kernel {
98
131
  this.sseBridge = null
99
132
  this.layoutComponents = null
100
133
  this.securityChecker = null
134
+ /** @type {import('./ModuleLoader.js').ModuleLoader|null} */
135
+ this.moduleLoader = null
136
+ /** @type {Map<string|symbol, any>} Pending provides from modules (applied after vueApp creation) */
137
+ this._pendingProvides = new Map()
138
+ /** @type {Map<string, import('vue').Component>} Pending components from modules */
139
+ this._pendingComponents = new Map()
101
140
  }
102
141
 
103
142
  /**
104
143
  * Create and configure the Vue app
144
+ *
145
+ * Note: This method is synchronous for backward compatibility.
146
+ * New-style modules (moduleDefs) are loaded synchronously via _loadModules().
147
+ * For async module loading, use createAppAsync() instead.
148
+ *
105
149
  * @returns {App} Vue app instance ready to mount
106
150
  */
107
151
  createApp() {
@@ -112,23 +156,72 @@ export class Kernel {
112
156
  this._createDeferredRegistry()
113
157
  // 2. Register auth:ready deferred (if auth configured)
114
158
  this._registerAuthDeferred()
115
- // 3. Initialize modules (can use all services, registers routes)
159
+ // 3. Initialize legacy modules (can use all services, registers routes)
116
160
  this._initModules()
161
+ // 3.5. Load new-style modules (moduleDefs) - synchronous for backward compat
162
+ this._loadModulesSync()
117
163
  // 4. Create router (needs routes from modules)
118
164
  this._createRouter()
165
+ // 4.5. Setup auth guard (if authAdapter provided)
166
+ this._setupAuthGuard()
119
167
  // 5. Setup auth:expired handler (needs router + authAdapter)
120
168
  this._setupAuthExpiredHandler()
121
169
  // 6. Create orchestrator and remaining components
122
170
  this._createOrchestrator()
123
- // 7. Create EventRouter (needs signals + orchestrator)
171
+ // 7. Wire modules that need orchestrator (phase 2)
172
+ this._wireModules()
173
+ // 8. Create EventRouter (needs signals + orchestrator)
124
174
  this._createEventRouter()
125
- // 8. Create SSEBridge (needs signals + authAdapter for token)
175
+ // 9. Create SSEBridge (needs signals + authAdapter for token)
126
176
  this._createSSEBridge()
127
177
  this._setupSecurity()
128
178
  this._createLayoutComponents()
129
179
  this._createVueApp()
130
180
  this._installPlugins()
131
- // 9. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
181
+ // 10. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
182
+ this._fireWarmups()
183
+ return this.vueApp
184
+ }
185
+
186
+ /**
187
+ * Create and configure the Vue app asynchronously
188
+ *
189
+ * Use this method when modules have async connect() methods.
190
+ * Supports the full async module loading flow.
191
+ *
192
+ * @returns {Promise<App>} Vue app instance ready to mount
193
+ */
194
+ async createAppAsync() {
195
+ // 1. Create services first (modules need them)
196
+ this._createSignalBus()
197
+ this._createHookRegistry()
198
+ this._createZoneRegistry()
199
+ this._createDeferredRegistry()
200
+ // 2. Register auth:ready deferred (if auth configured)
201
+ this._registerAuthDeferred()
202
+ // 3. Initialize legacy modules (can use all services, registers routes)
203
+ this._initModules()
204
+ // 3.5. Load new-style modules (moduleDefs) - async version
205
+ await this._loadModules()
206
+ // 4. Create router (needs routes from modules)
207
+ this._createRouter()
208
+ // 4.5. Setup auth guard (if authAdapter provided)
209
+ this._setupAuthGuard()
210
+ // 5. Setup auth:expired handler (needs router + authAdapter)
211
+ this._setupAuthExpiredHandler()
212
+ // 6. Create orchestrator and remaining components
213
+ this._createOrchestrator()
214
+ // 7. Wire modules that need orchestrator (phase 2)
215
+ await this._wireModulesAsync()
216
+ // 8. Create EventRouter (needs signals + orchestrator)
217
+ this._createEventRouter()
218
+ // 9. Create SSEBridge (needs signals + authAdapter for token)
219
+ this._createSSEBridge()
220
+ this._setupSecurity()
221
+ this._createLayoutComponents()
222
+ this._createVueApp()
223
+ this._installPlugins()
224
+ // 10. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
132
225
  this._fireWarmups()
133
226
  return this.vueApp
134
227
  }
@@ -217,7 +310,7 @@ export class Kernel {
217
310
  }
218
311
 
219
312
  /**
220
- * Initialize modules from glob import
313
+ * Initialize legacy modules from glob import
221
314
  * Passes services to modules for zone/signal/hook registration
222
315
  */
223
316
  _initModules() {
@@ -236,15 +329,168 @@ export class Kernel {
236
329
  }
237
330
 
238
331
  /**
239
- * Create Vue Router with auth guard
332
+ * Create KernelContext for module connection
333
+ *
334
+ * Creates a context object that provides modules access to kernel services
335
+ * and registration APIs.
336
+ *
337
+ * @param {import('./Module.js').Module} module - Module instance
338
+ * @returns {import('./KernelContext.js').KernelContext}
339
+ * @private
240
340
  */
241
- _createRouter() {
242
- const { pages, homeRoute, coreRoutes, basePath, hashMode, authAdapter } = this.options
341
+ _createModuleContext(module) {
342
+ return createKernelContext(this, module)
343
+ }
243
344
 
244
- // Validate required pages
245
- if (!pages?.login) {
246
- throw new Error('[Kernel] pages.login is required')
345
+ /**
346
+ * Load new-style modules synchronously
347
+ *
348
+ * Used by createApp() for backward compatibility. Modules with async
349
+ * connect() methods will have their promises ignored (fire-and-forget).
350
+ * For proper async support, use createAppAsync().
351
+ *
352
+ * @private
353
+ */
354
+ _loadModulesSync() {
355
+ const { moduleDefs } = this.options
356
+ if (!moduleDefs?.length) return
357
+
358
+ this.moduleLoader = createModuleLoader()
359
+
360
+ // Add all modules
361
+ for (const mod of moduleDefs) {
362
+ this.moduleLoader.add(mod)
363
+ }
364
+
365
+ // Get sorted modules and load synchronously
366
+ const sorted = this.moduleLoader._topologicalSort()
367
+
368
+ for (const name of sorted) {
369
+ const module = this.moduleLoader._registered.get(name)
370
+ const ctx = this._createModuleContext(module)
371
+
372
+ // Check if enabled
373
+ if (!module.enabled(ctx)) {
374
+ continue
375
+ }
376
+
377
+ // Connect module (sync - async modules will be fire-and-forget)
378
+ const result = module.connect(ctx)
379
+
380
+ // If it's a promise, we can't await it in sync context
381
+ // The module should handle its own async initialization
382
+ if (result instanceof Promise) {
383
+ result.catch(err => {
384
+ console.error(`[Kernel] Async module '${name}' failed:`, err)
385
+ })
386
+ }
387
+
388
+ this.moduleLoader._loaded.set(name, module)
389
+ this.moduleLoader._loadOrder.push(name)
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Load new-style modules asynchronously
395
+ *
396
+ * Fully supports async connect() methods in modules.
397
+ * Used by createAppAsync().
398
+ *
399
+ * @returns {Promise<void>}
400
+ * @private
401
+ */
402
+ async _loadModules() {
403
+ const { moduleDefs } = this.options
404
+ if (!moduleDefs?.length) return
405
+
406
+ this.moduleLoader = createModuleLoader()
407
+
408
+ // Add all modules
409
+ for (const mod of moduleDefs) {
410
+ this.moduleLoader.add(mod)
411
+ }
412
+
413
+ // Get sorted modules and load
414
+ const sorted = this.moduleLoader._topologicalSort()
415
+
416
+ for (const name of sorted) {
417
+ const module = this.moduleLoader._registered.get(name)
418
+ const ctx = this._createModuleContext(module)
419
+
420
+ // Check if enabled
421
+ if (!module.enabled(ctx)) {
422
+ continue
423
+ }
424
+
425
+ // Connect module (await async)
426
+ await module.connect(ctx)
427
+
428
+ this.moduleLoader._loaded.set(name, module)
429
+ this.moduleLoader._loadOrder.push(name)
247
430
  }
431
+ }
432
+
433
+ /**
434
+ * Wire modules that need orchestrator (phase 2)
435
+ *
436
+ * Some modules may need access to the orchestrator after it's created.
437
+ * This method emits 'kernel:ready' signal that modules can listen to.
438
+ *
439
+ * @private
440
+ */
441
+ _wireModules() {
442
+ if (!this.moduleLoader) return
443
+
444
+ // Emit kernel:ready signal for modules that need orchestrator
445
+ const result = this.signals.emit('kernel:ready', {
446
+ orchestrator: this.orchestrator,
447
+ kernel: this
448
+ })
449
+
450
+ // If emit returns a promise (async handlers), we can't await it
451
+ if (result instanceof Promise) {
452
+ result.catch(err => {
453
+ console.error('[Kernel] kernel:ready handler failed:', err)
454
+ })
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Wire modules that need orchestrator (phase 2) - async version
460
+ *
461
+ * @returns {Promise<void>}
462
+ * @private
463
+ */
464
+ async _wireModulesAsync() {
465
+ if (!this.moduleLoader) return
466
+
467
+ // Emit kernel:ready signal for modules that need orchestrator
468
+ await this.signals.emit('kernel:ready', {
469
+ orchestrator: this.orchestrator,
470
+ kernel: this
471
+ })
472
+ }
473
+
474
+ /**
475
+ * Create Vue Router
476
+ *
477
+ * Routes are built from:
478
+ * 1. Module routes (via ctx.routes()) - can be public or protected via meta
479
+ * 2. coreRoutes option - additional routes
480
+ * 3. homeRoute - redirect or component for /
481
+ *
482
+ * Auth is NOT hardcoded here - it's handled via:
483
+ * - Route meta: { public: true } or { requiresAuth: false }
484
+ * - Auth guard registered via _setupAuthGuard() if authAdapter provided
485
+ *
486
+ * Layout modes:
487
+ * - **Shell mode** (pages.shell): All routes inside shell, layout wraps protected area
488
+ * - **Layout-only mode**: Layout at root with all routes as children
489
+ */
490
+ _createRouter() {
491
+ const { pages, homeRoute, coreRoutes, basePath, hashMode } = this.options
492
+
493
+ // Layout is required (shell is optional)
248
494
  if (!pages?.layout) {
249
495
  throw new Error('[Kernel] pages.layout is required')
250
496
  }
@@ -252,50 +498,115 @@ export class Kernel {
252
498
  // Build home route
253
499
  let homeRouteConfig
254
500
  if (typeof homeRoute === 'object' && homeRoute.component) {
255
- // homeRoute is a route config with component
256
501
  homeRouteConfig = { path: '', ...homeRoute }
257
502
  } else {
258
- // homeRoute is a route name for redirect
259
503
  homeRouteConfig = { path: '', redirect: { name: homeRoute || 'home' } }
260
504
  }
261
505
 
262
- // Build layout children: home + coreRoutes + moduleRoutes
506
+ // Collect all module routes
507
+ const moduleRoutes = getRoutes()
508
+
509
+ // Separate public routes (meta.public or meta.requiresAuth === false)
510
+ const publicRoutes = moduleRoutes.filter(r => r.meta?.public || r.meta?.requiresAuth === false)
511
+ const protectedRoutes = moduleRoutes.filter(r => !r.meta?.public && r.meta?.requiresAuth !== false)
512
+
513
+ // Auto-create login route if pages.login is provided (backward compatibility)
514
+ // This creates a public route that modules don't need to define explicitly.
515
+ if (pages.login) {
516
+ publicRoutes.unshift({
517
+ path: '/login',
518
+ name: 'login',
519
+ component: pages.login,
520
+ meta: { public: true }
521
+ })
522
+ }
523
+
524
+ // Build layout children: home + coreRoutes + protected module routes
263
525
  const layoutChildren = [
264
526
  homeRouteConfig,
265
527
  ...(coreRoutes || []),
266
- ...getRoutes()
528
+ ...protectedRoutes
267
529
  ]
268
530
 
269
- // Build routes
270
- const routes = [
271
- {
272
- path: '/login',
273
- name: 'login',
274
- component: pages.login
275
- },
276
- {
277
- path: '/',
278
- component: pages.layout,
279
- meta: { requiresAuth: true },
280
- children: layoutChildren
281
- }
282
- ]
531
+ let routes
532
+
533
+ if (pages.shell) {
534
+ // Shell mode: shell wraps everything, layout wraps protected area
535
+ routes = [
536
+ {
537
+ path: '/',
538
+ component: pages.shell,
539
+ children: [
540
+ // Public routes at shell level (login, register, etc.)
541
+ ...publicRoutes,
542
+ // Protected area with layout
543
+ {
544
+ path: '',
545
+ component: pages.layout,
546
+ meta: { requiresAuth: true },
547
+ children: layoutChildren
548
+ }
549
+ ]
550
+ }
551
+ ]
552
+ } else {
553
+ // Layout-only mode: layout at root
554
+ // Public routes need to be handled differently (no shell)
555
+ routes = [
556
+ // Public routes standalone
557
+ ...publicRoutes,
558
+ // Protected area with layout
559
+ {
560
+ path: '/',
561
+ component: pages.layout,
562
+ meta: { requiresAuth: true },
563
+ children: layoutChildren
564
+ }
565
+ ]
566
+ }
283
567
 
284
568
  this.router = createRouter({
285
569
  history: hashMode ? createWebHashHistory(basePath) : createWebHistory(basePath),
286
570
  routes
287
571
  })
572
+ }
288
573
 
289
- // Auth guard
290
- if (authAdapter) {
291
- this.router.beforeEach((to, from, next) => {
292
- if (to.meta.requiresAuth && !authAdapter.isAuthenticated()) {
293
- next({ name: 'login' })
294
- } else {
295
- next()
296
- }
297
- })
298
- }
574
+ /**
575
+ * Setup auth guard on router
576
+ *
577
+ * Only installed if authAdapter is provided.
578
+ * Uses route meta to determine access:
579
+ * - meta.public: true → always accessible
580
+ * - meta.requiresAuth: false → always accessible
581
+ * - meta.requiresAuth: true → requires auth (default for layout children)
582
+ * - no meta → inherits from parent route
583
+ */
584
+ _setupAuthGuard() {
585
+ const { authAdapter } = this.options
586
+ if (!authAdapter) return
587
+
588
+ this.router.beforeEach((to, from, next) => {
589
+ // Check if route or any parent is explicitly public
590
+ const isPublic = to.matched.some(record =>
591
+ record.meta.public === true || record.meta.requiresAuth === false
592
+ )
593
+
594
+ if (isPublic) {
595
+ next()
596
+ return
597
+ }
598
+
599
+ // Check if route or any parent requires auth
600
+ const requiresAuth = to.matched.some(record => record.meta.requiresAuth === true)
601
+
602
+ if (requiresAuth && !authAdapter.isAuthenticated()) {
603
+ // Redirect to login (if exists) or emit signal
604
+ const loginRoute = this.router.hasRoute('login') ? { name: 'login' } : '/'
605
+ next(loginRoute)
606
+ } else {
607
+ next()
608
+ }
609
+ })
299
610
  }
300
611
 
301
612
  /**
@@ -390,12 +701,11 @@ export class Kernel {
390
701
 
391
702
  /**
392
703
  * Create zone registry for extensible UI composition
393
- * Registers standard zones during bootstrap.
704
+ * Zones are created dynamically when used (no pre-registration).
394
705
  */
395
706
  _createZoneRegistry() {
396
707
  const debug = this.options.debug ?? false
397
708
  this.zoneRegistry = createZoneRegistry({ debug })
398
- registerStandardZones(this.zoneRegistry)
399
709
  }
400
710
 
401
711
  /**
@@ -489,12 +799,29 @@ export class Kernel {
489
799
 
490
800
  /**
491
801
  * Create Vue app instance
802
+ *
803
+ * When debugBar is enabled, wraps the root component to include QdadmDebugBar
804
+ * at the app level, ensuring it's visible on all pages (including login).
492
805
  */
493
806
  _createVueApp() {
494
807
  if (!this.options.root) {
495
808
  throw new Error('[Kernel] root component is required')
496
809
  }
497
- this.vueApp = createApp(this.options.root)
810
+
811
+ // If debugBar is enabled and component provided, wrap root with DebugBar
812
+ if (this.options.debugBar?.component && QdadmDebugBar) {
813
+ const OriginalRoot = this.options.root
814
+ const DebugBarComponent = QdadmDebugBar
815
+ const WrappedRoot = {
816
+ name: 'QdadmRootWrapper',
817
+ render() {
818
+ return [h(OriginalRoot), h(DebugBarComponent)]
819
+ }
820
+ }
821
+ this.vueApp = createApp(WrappedRoot)
822
+ } else {
823
+ this.vueApp = createApp(this.options.root)
824
+ }
498
825
  }
499
826
 
500
827
  /**
@@ -527,6 +854,18 @@ export class Kernel {
527
854
  // Router
528
855
  app.use(this.router)
529
856
 
857
+ // Apply pending provides from modules (registered before vueApp existed)
858
+ for (const [key, value] of this._pendingProvides) {
859
+ app.provide(key, value)
860
+ }
861
+ this._pendingProvides.clear()
862
+
863
+ // Apply pending components from modules
864
+ for (const [name, component] of this._pendingComponents) {
865
+ app.component(name, component)
866
+ }
867
+ this._pendingComponents.clear()
868
+
530
869
  // Extract home route name for breadcrumb
531
870
  const { homeRoute } = this.options
532
871
  const homeRouteName = typeof homeRoute === 'object' ? homeRoute.name : homeRoute
@@ -534,10 +873,24 @@ export class Kernel {
534
873
  // Zone registry injection
535
874
  app.provide('qdadmZoneRegistry', this.zoneRegistry)
536
875
 
537
- // Dev mode: expose zone registry on window for DevTools inspection
876
+ // Dev mode: expose qdadm services on window for DevTools inspection
538
877
  if (this.options.debug && typeof window !== 'undefined') {
878
+ window.__qdadm = {
879
+ kernel: this,
880
+ orchestrator: this.orchestrator,
881
+ signals: this.signals,
882
+ hooks: this.hookRegistry,
883
+ zones: this.zoneRegistry,
884
+ deferred: this.deferred,
885
+ router: this.router,
886
+ // Helper to get a manager quickly
887
+ get: (name) => this.orchestrator.get(name),
888
+ // List all managers
889
+ managers: () => this.orchestrator.getRegisteredNames()
890
+ }
891
+ // Legacy alias
539
892
  window.__qdadmZones = this.zoneRegistry
540
- console.debug('[qdadm] Zone registry exposed on window.__qdadmZones')
893
+ console.debug('[qdadm] Debug mode: window.__qdadm exposed (orchestrator, signals, hooks, zones, deferred, router)')
541
894
  }
542
895
 
543
896
  // Signal bus injection
@@ -697,6 +1050,41 @@ export class Kernel {
697
1050
  return this.securityChecker
698
1051
  }
699
1052
 
1053
+ /**
1054
+ * Get the ModuleLoader instance
1055
+ * @returns {import('./ModuleLoader.js').ModuleLoader|null}
1056
+ */
1057
+ getModuleLoader() {
1058
+ return this.moduleLoader
1059
+ }
1060
+
1061
+ /**
1062
+ * Shorthand accessor for module loader
1063
+ * Allows `kernel.modules.getModules()` syntax
1064
+ * @returns {import('./ModuleLoader.js').ModuleLoader|null}
1065
+ */
1066
+ get modules() {
1067
+ return this.moduleLoader
1068
+ }
1069
+
1070
+ /**
1071
+ * Get the DebugModule instance if loaded
1072
+ * @returns {import('../debug/DebugModule.js').DebugModule|null}
1073
+ */
1074
+ getDebugModule() {
1075
+ return this.moduleLoader?._loaded?.get('debug') ?? null
1076
+ }
1077
+
1078
+ /**
1079
+ * Shorthand accessor for debug bridge
1080
+ * Allows `kernel.debugBar.toggle()` syntax
1081
+ * @returns {import('../debug/DebugBridge.js').DebugBridge|null}
1082
+ */
1083
+ get debugBar() {
1084
+ const debugModule = this.getDebugModule()
1085
+ return debugModule?.getBridge?.() ?? null
1086
+ }
1087
+
700
1088
  /**
701
1089
  * Setup an axios client with automatic auth and error handling
702
1090
  *