qdadm 0.29.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 (49) hide show
  1. package/package.json +11 -2
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/index.js +5 -3
  4. package/src/components/layout/AppLayout.vue +13 -1
  5. package/src/components/layout/Zone.vue +40 -23
  6. package/src/composables/index.js +2 -1
  7. package/src/composables/useAuth.js +43 -4
  8. package/src/composables/useCurrentEntity.js +44 -0
  9. package/src/composables/useFormPageBuilder.js +3 -3
  10. package/src/composables/useNavContext.js +24 -8
  11. package/src/composables/useSSEBridge.js +118 -0
  12. package/src/debug/AuthCollector.js +254 -0
  13. package/src/debug/Collector.js +235 -0
  14. package/src/debug/DebugBridge.js +163 -0
  15. package/src/debug/DebugModule.js +215 -0
  16. package/src/debug/EntitiesCollector.js +376 -0
  17. package/src/debug/ErrorCollector.js +66 -0
  18. package/src/debug/LocalStorageAdapter.js +150 -0
  19. package/src/debug/SignalCollector.js +87 -0
  20. package/src/debug/ToastCollector.js +82 -0
  21. package/src/debug/ZonesCollector.js +300 -0
  22. package/src/debug/components/DebugBar.vue +1232 -0
  23. package/src/debug/components/ObjectTree.vue +194 -0
  24. package/src/debug/components/index.js +8 -0
  25. package/src/debug/components/panels/AuthPanel.vue +103 -0
  26. package/src/debug/components/panels/EntitiesPanel.vue +616 -0
  27. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  28. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  29. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  30. package/src/debug/components/panels/index.js +8 -0
  31. package/src/debug/index.js +31 -0
  32. package/src/editors/index.js +12 -0
  33. package/src/entity/EntityManager.js +142 -20
  34. package/src/entity/storage/MockApiStorage.js +17 -1
  35. package/src/entity/storage/index.js +9 -2
  36. package/src/gen/index.js +3 -0
  37. package/src/index.js +7 -0
  38. package/src/kernel/Kernel.js +661 -48
  39. package/src/kernel/KernelContext.js +385 -0
  40. package/src/kernel/Module.js +111 -0
  41. package/src/kernel/ModuleLoader.js +573 -0
  42. package/src/kernel/SSEBridge.js +354 -0
  43. package/src/kernel/SignalBus.js +10 -7
  44. package/src/kernel/index.js +19 -0
  45. package/src/toast/ToastBridgeModule.js +70 -0
  46. package/src/toast/ToastListener.vue +47 -0
  47. package/src/toast/index.js +15 -0
  48. package/src/toast/useSignalToast.js +113 -0
  49. package/src/composables/useSSE.js +0 -212
@@ -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'
@@ -56,13 +58,20 @@ import { createManagers } from '../entity/factory.js'
56
58
  import { defaultStorageResolver } from '../entity/storage/factory.js'
57
59
  import { createDeferredRegistry } from '../deferred/DeferredRegistry.js'
58
60
  import { createEventRouter } from './EventRouter.js'
61
+ import { createSSEBridge } from './SSEBridge.js'
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
59
67
 
60
68
  export class Kernel {
61
69
  /**
62
70
  * @param {object} options
63
71
  * @param {object} options.root - Root Vue component
64
- * @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)
65
73
  * @param {object} options.modulesOptions - Options for initModules (e.g., { coreNavItems })
74
+ * @param {Array} options.moduleDefs - New-style module definitions (Module classes/objects/functions)
66
75
  * @param {string[]} options.sectionOrder - Navigation section order
67
76
  * @param {object} options.managers - Entity managers { name: config } - can be instances, strings, or config objects
68
77
  * @param {object} options.managerRegistry - Registry of manager classes from qdadm-gen { name: ManagerClass }
@@ -70,7 +79,9 @@ export class Kernel {
70
79
  * @param {function} options.managerResolver - Custom manager resolver (config, entityName, context) => Manager
71
80
  * @param {object} options.authAdapter - Auth adapter for login/logout (app-level authentication)
72
81
  * @param {object} options.entityAuthAdapter - Auth adapter for entity permissions (scope/silo checks)
73
- * @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)
74
85
  * @param {string} options.homeRoute - Route name for home redirect (or object { name, component })
75
86
  * @param {Array} options.coreRoutes - Additional routes as layout children (before module routes)
76
87
  * @param {string} options.basePath - Base path for router (e.g., '/dashboard/')
@@ -82,8 +93,32 @@ export class Kernel {
82
93
  * @param {object} options.security - Security config { role_hierarchy, role_permissions, entity_permissions }
83
94
  * @param {boolean} options.warmup - Enable warmup at boot (default: true)
84
95
  * @param {object} options.eventRouter - EventRouter config { 'source:signal': ['target:signal', ...] }
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 }
85
98
  */
86
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
+ }
87
122
  this.options = options
88
123
  this.vueApp = null
89
124
  this.router = null
@@ -93,12 +128,24 @@ export class Kernel {
93
128
  this.hookRegistry = null
94
129
  this.deferred = null
95
130
  this.eventRouter = null
131
+ this.sseBridge = null
96
132
  this.layoutComponents = null
97
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()
98
140
  }
99
141
 
100
142
  /**
101
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
+ *
102
149
  * @returns {App} Vue app instance ready to mount
103
150
  */
104
151
  createApp() {
@@ -109,19 +156,72 @@ export class Kernel {
109
156
  this._createDeferredRegistry()
110
157
  // 2. Register auth:ready deferred (if auth configured)
111
158
  this._registerAuthDeferred()
112
- // 3. Initialize modules (can use all services, registers routes)
159
+ // 3. Initialize legacy modules (can use all services, registers routes)
113
160
  this._initModules()
161
+ // 3.5. Load new-style modules (moduleDefs) - synchronous for backward compat
162
+ this._loadModulesSync()
114
163
  // 4. Create router (needs routes from modules)
115
164
  this._createRouter()
116
- // 5. Create orchestrator and remaining components
165
+ // 4.5. Setup auth guard (if authAdapter provided)
166
+ this._setupAuthGuard()
167
+ // 5. Setup auth:expired handler (needs router + authAdapter)
168
+ this._setupAuthExpiredHandler()
169
+ // 6. Create orchestrator and remaining components
117
170
  this._createOrchestrator()
118
- // 6. Create EventRouter (needs signals + orchestrator)
171
+ // 7. Wire modules that need orchestrator (phase 2)
172
+ this._wireModules()
173
+ // 8. Create EventRouter (needs signals + orchestrator)
119
174
  this._createEventRouter()
175
+ // 9. Create SSEBridge (needs signals + authAdapter for token)
176
+ this._createSSEBridge()
120
177
  this._setupSecurity()
121
178
  this._createLayoutComponents()
122
179
  this._createVueApp()
123
180
  this._installPlugins()
124
- // 6. 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)
125
225
  this._fireWarmups()
126
226
  return this.vueApp
127
227
  }
@@ -144,6 +244,58 @@ export class Kernel {
144
244
  })
145
245
  }
146
246
 
247
+ /**
248
+ * Setup handler for auth:expired signal
249
+ *
250
+ * When auth:expired is emitted (e.g., from API 401/403 response),
251
+ * this handler:
252
+ * 1. Calls authAdapter.logout() to clear tokens
253
+ * 2. Redirects to login page
254
+ * 3. Optionally calls onAuthExpired callback
255
+ *
256
+ * To emit auth:expired from your API client:
257
+ * ```js
258
+ * axios.interceptors.response.use(
259
+ * response => response,
260
+ * error => {
261
+ * if (error.response?.status === 401 || error.response?.status === 403) {
262
+ * signals.emit('auth:expired', { status: error.response.status })
263
+ * }
264
+ * return Promise.reject(error)
265
+ * }
266
+ * )
267
+ * ```
268
+ */
269
+ _setupAuthExpiredHandler() {
270
+ const { authAdapter, onAuthExpired } = this.options
271
+ if (!authAdapter) return
272
+
273
+ this.signals.on('auth:expired', async (payload) => {
274
+ const debug = this.options.debug ?? false
275
+ if (debug) {
276
+ console.warn('[Kernel] auth:expired received:', payload)
277
+ }
278
+
279
+ // 1. Logout (clear tokens)
280
+ if (authAdapter.logout) {
281
+ authAdapter.logout()
282
+ }
283
+
284
+ // 2. Emit auth:logout signal
285
+ await this.signals.emit('auth:logout', { reason: 'expired', ...payload })
286
+
287
+ // 3. Redirect to login (if not already there)
288
+ if (this.router.currentRoute.value.name !== 'login') {
289
+ this.router.push({ name: 'login', query: { expired: '1' } })
290
+ }
291
+
292
+ // 4. Optional callback
293
+ if (onAuthExpired) {
294
+ onAuthExpired(payload)
295
+ }
296
+ })
297
+ }
298
+
147
299
  /**
148
300
  * Fire entity cache warmups
149
301
  * Fire-and-forget: pages that need cache will await via DeferredRegistry.
@@ -158,7 +310,7 @@ export class Kernel {
158
310
  }
159
311
 
160
312
  /**
161
- * Initialize modules from glob import
313
+ * Initialize legacy modules from glob import
162
314
  * Passes services to modules for zone/signal/hook registration
163
315
  */
164
316
  _initModules() {
@@ -177,15 +329,168 @@ export class Kernel {
177
329
  }
178
330
 
179
331
  /**
180
- * 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
181
340
  */
182
- _createRouter() {
183
- const { pages, homeRoute, coreRoutes, basePath, hashMode, authAdapter } = this.options
341
+ _createModuleContext(module) {
342
+ return createKernelContext(this, module)
343
+ }
184
344
 
185
- // Validate required pages
186
- if (!pages?.login) {
187
- 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)
188
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)
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)
189
494
  if (!pages?.layout) {
190
495
  throw new Error('[Kernel] pages.layout is required')
191
496
  }
@@ -193,50 +498,115 @@ export class Kernel {
193
498
  // Build home route
194
499
  let homeRouteConfig
195
500
  if (typeof homeRoute === 'object' && homeRoute.component) {
196
- // homeRoute is a route config with component
197
501
  homeRouteConfig = { path: '', ...homeRoute }
198
502
  } else {
199
- // homeRoute is a route name for redirect
200
503
  homeRouteConfig = { path: '', redirect: { name: homeRoute || 'home' } }
201
504
  }
202
505
 
203
- // 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
204
525
  const layoutChildren = [
205
526
  homeRouteConfig,
206
527
  ...(coreRoutes || []),
207
- ...getRoutes()
528
+ ...protectedRoutes
208
529
  ]
209
530
 
210
- // Build routes
211
- const routes = [
212
- {
213
- path: '/login',
214
- name: 'login',
215
- component: pages.login
216
- },
217
- {
218
- path: '/',
219
- component: pages.layout,
220
- meta: { requiresAuth: true },
221
- children: layoutChildren
222
- }
223
- ]
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
+ }
224
567
 
225
568
  this.router = createRouter({
226
569
  history: hashMode ? createWebHashHistory(basePath) : createWebHistory(basePath),
227
570
  routes
228
571
  })
572
+ }
229
573
 
230
- // Auth guard
231
- if (authAdapter) {
232
- this.router.beforeEach((to, from, next) => {
233
- if (to.meta.requiresAuth && !authAdapter.isAuthenticated()) {
234
- next({ name: 'login' })
235
- } else {
236
- next()
237
- }
238
- })
239
- }
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
+ })
240
610
  }
241
611
 
242
612
  /**
@@ -331,12 +701,11 @@ export class Kernel {
331
701
 
332
702
  /**
333
703
  * Create zone registry for extensible UI composition
334
- * Registers standard zones during bootstrap.
704
+ * Zones are created dynamically when used (no pre-registration).
335
705
  */
336
706
  _createZoneRegistry() {
337
707
  const debug = this.options.debug ?? false
338
708
  this.zoneRegistry = createZoneRegistry({ debug })
339
- registerStandardZones(this.zoneRegistry)
340
709
  }
341
710
 
342
711
  /**
@@ -368,6 +737,52 @@ export class Kernel {
368
737
  })
369
738
  }
370
739
 
740
+ /**
741
+ * Create SSEBridge for Server-Sent Events to SignalBus integration
742
+ *
743
+ * SSE config:
744
+ * ```js
745
+ * sse: {
746
+ * url: '/api/events', // SSE endpoint
747
+ * reconnectDelay: 5000, // Reconnect delay (ms), 0 to disable
748
+ * signalPrefix: 'sse', // Signal prefix (default: 'sse')
749
+ * autoConnect: false, // Connect immediately vs wait for auth:login
750
+ * events: ['task:completed', 'bot:status'] // Event names to register
751
+ * }
752
+ * ```
753
+ */
754
+ _createSSEBridge() {
755
+ const { sse, authAdapter } = this.options
756
+ if (!sse?.url) return
757
+
758
+ const debug = this.options.debug ?? false
759
+
760
+ // Build getToken from authAdapter
761
+ const getToken = authAdapter?.getToken
762
+ ? () => authAdapter.getToken()
763
+ : () => localStorage.getItem('auth_token')
764
+
765
+ this.sseBridge = createSSEBridge({
766
+ signals: this.signals,
767
+ url: sse.url,
768
+ reconnectDelay: sse.reconnectDelay ?? 5000,
769
+ signalPrefix: sse.signalPrefix ?? 'sse',
770
+ autoConnect: sse.autoConnect ?? false,
771
+ withCredentials: sse.withCredentials ?? false,
772
+ tokenParam: sse.tokenParam ?? 'token',
773
+ getToken,
774
+ debug
775
+ })
776
+
777
+ // Register known event names if provided
778
+ if (sse.events?.length) {
779
+ // Wait for connection before registering
780
+ this.signals.once('sse:connected').then(() => {
781
+ this.sseBridge.registerEvents(sse.events)
782
+ })
783
+ }
784
+ }
785
+
371
786
  /**
372
787
  * Create layout components map for useLayoutResolver
373
788
  * Maps layout types to their Vue components.
@@ -384,12 +799,29 @@ export class Kernel {
384
799
 
385
800
  /**
386
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).
387
805
  */
388
806
  _createVueApp() {
389
807
  if (!this.options.root) {
390
808
  throw new Error('[Kernel] root component is required')
391
809
  }
392
- 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
+ }
393
825
  }
394
826
 
395
827
  /**
@@ -422,6 +854,18 @@ export class Kernel {
422
854
  // Router
423
855
  app.use(this.router)
424
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
+
425
869
  // Extract home route name for breadcrumb
426
870
  const { homeRoute } = this.options
427
871
  const homeRouteName = typeof homeRoute === 'object' ? homeRoute.name : homeRoute
@@ -429,15 +873,34 @@ export class Kernel {
429
873
  // Zone registry injection
430
874
  app.provide('qdadmZoneRegistry', this.zoneRegistry)
431
875
 
432
- // Dev mode: expose zone registry on window for DevTools inspection
876
+ // Dev mode: expose qdadm services on window for DevTools inspection
433
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
434
892
  window.__qdadmZones = this.zoneRegistry
435
- console.debug('[qdadm] Zone registry exposed on window.__qdadmZones')
893
+ console.debug('[qdadm] Debug mode: window.__qdadm exposed (orchestrator, signals, hooks, zones, deferred, router)')
436
894
  }
437
895
 
438
896
  // Signal bus injection
439
897
  app.provide('qdadmSignals', this.signals)
440
898
 
899
+ // SSEBridge injection (if configured)
900
+ if (this.sseBridge) {
901
+ app.provide('qdadmSSEBridge', this.sseBridge)
902
+ }
903
+
441
904
  // Hook registry injection
442
905
  app.provide('qdadmHooks', this.hookRegistry)
443
906
 
@@ -538,6 +1001,22 @@ export class Kernel {
538
1001
  return this.eventRouter
539
1002
  }
540
1003
 
1004
+ /**
1005
+ * Get the SSEBridge instance
1006
+ * @returns {import('./SSEBridge.js').SSEBridge|null}
1007
+ */
1008
+ getSSEBridge() {
1009
+ return this.sseBridge
1010
+ }
1011
+
1012
+ /**
1013
+ * Shorthand accessor for SSE bridge
1014
+ * @returns {import('./SSEBridge.js').SSEBridge|null}
1015
+ */
1016
+ get sse() {
1017
+ return this.sseBridge
1018
+ }
1019
+
541
1020
  /**
542
1021
  * Get the layout components map
543
1022
  * @returns {object} Layout components by type
@@ -570,4 +1049,138 @@ export class Kernel {
570
1049
  get security() {
571
1050
  return this.securityChecker
572
1051
  }
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
+
1088
+ /**
1089
+ * Setup an axios client with automatic auth and error handling
1090
+ *
1091
+ * Adds interceptors that:
1092
+ * - Add Authorization header with token from authAdapter
1093
+ * - Emit auth:expired on 401/403 responses (triggers auto-logout)
1094
+ * - Emit api:error on all errors for centralized handling
1095
+ *
1096
+ * Usage:
1097
+ * ```js
1098
+ * import axios from 'axios'
1099
+ *
1100
+ * const apiClient = axios.create({ baseURL: '/api' })
1101
+ * kernel.setupApiClient(apiClient)
1102
+ *
1103
+ * // Now 401/403 errors automatically trigger logout
1104
+ * const storage = new ApiStorage({ endpoint: '/users', client: apiClient })
1105
+ * ```
1106
+ *
1107
+ * Or let Kernel create the client:
1108
+ * ```js
1109
+ * const kernel = new Kernel({
1110
+ * ...options,
1111
+ * apiClient: { baseURL: '/api' } // axios.create() options
1112
+ * })
1113
+ * const apiClient = kernel.getApiClient()
1114
+ * ```
1115
+ *
1116
+ * @param {object} client - Axios instance to configure
1117
+ * @returns {object} The configured axios instance
1118
+ */
1119
+ setupApiClient(client) {
1120
+ const { authAdapter } = this.options
1121
+ const signals = this.signals
1122
+ const debug = this.options.debug ?? false
1123
+
1124
+ // Request interceptor: add Authorization header
1125
+ client.interceptors.request.use(
1126
+ (config) => {
1127
+ if (authAdapter?.getToken) {
1128
+ const token = authAdapter.getToken()
1129
+ if (token) {
1130
+ config.headers = config.headers || {}
1131
+ config.headers.Authorization = `Bearer ${token}`
1132
+ }
1133
+ }
1134
+ return config
1135
+ },
1136
+ (error) => Promise.reject(error)
1137
+ )
1138
+
1139
+ // Response interceptor: handle auth errors
1140
+ client.interceptors.response.use(
1141
+ (response) => response,
1142
+ async (error) => {
1143
+ const status = error.response?.status
1144
+ const url = error.config?.url
1145
+
1146
+ // Emit api:error for all errors
1147
+ await signals.emit('api:error', {
1148
+ status,
1149
+ message: error.message,
1150
+ url,
1151
+ error
1152
+ })
1153
+
1154
+ // Emit auth:expired for 401/403
1155
+ if (status === 401 || status === 403) {
1156
+ if (debug) {
1157
+ console.warn(`[Kernel] API ${status} error on ${url}, emitting auth:expired`)
1158
+ }
1159
+ await signals.emit('auth:expired', { status, url })
1160
+ }
1161
+
1162
+ return Promise.reject(error)
1163
+ }
1164
+ )
1165
+
1166
+ // Store reference
1167
+ this._apiClient = client
1168
+ return client
1169
+ }
1170
+
1171
+ /**
1172
+ * Get the configured API client
1173
+ * @returns {object|null} Axios instance or null if not configured
1174
+ */
1175
+ getApiClient() {
1176
+ return this._apiClient
1177
+ }
1178
+
1179
+ /**
1180
+ * Shorthand accessor for API client
1181
+ * @returns {object|null}
1182
+ */
1183
+ get api() {
1184
+ return this._apiClient
1185
+ }
573
1186
  }