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.
- package/package.json +11 -2
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/index.js +5 -3
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +2 -1
- package/src/composables/useAuth.js +43 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/composables/useSSEBridge.js +118 -0
- package/src/debug/AuthCollector.js +254 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +376 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +103 -0
- package/src/debug/components/panels/EntitiesPanel.vue +616 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/editors/index.js +12 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/storage/MockApiStorage.js +17 -1
- package/src/entity/storage/index.js +9 -2
- package/src/gen/index.js +3 -0
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +661 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SSEBridge.js +354 -0
- package/src/kernel/SignalBus.js +10 -7
- package/src/kernel/index.js +19 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
- package/src/composables/useSSE.js +0 -212
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DebugModule - Module System v2 class for debug tools integration
|
|
3
|
+
*
|
|
4
|
+
* Integrates the debug infrastructure (DebugBridge, collectors, DebugBar)
|
|
5
|
+
* into a qdadm application via the Module System.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Creates and configures DebugBridge with default collectors
|
|
9
|
+
* - Registers ErrorCollector and SignalCollector automatically
|
|
10
|
+
* - Adds DebugBar component to 'app:debug' zone
|
|
11
|
+
* - Provides debug bridge via Vue's provide/inject
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { createKernel, DebugModule } from 'qdadm'
|
|
15
|
+
*
|
|
16
|
+
* const kernel = createKernel({ debug: true })
|
|
17
|
+
* kernel.use(new DebugModule({ enabled: true }))
|
|
18
|
+
* await kernel.boot()
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { h, inject, defineComponent } from 'vue'
|
|
22
|
+
import { Module } from '../kernel/Module.js'
|
|
23
|
+
import { createDebugBridge } from './DebugBridge.js'
|
|
24
|
+
import { ErrorCollector } from './ErrorCollector.js'
|
|
25
|
+
import { SignalCollector } from './SignalCollector.js'
|
|
26
|
+
import { ToastCollector } from './ToastCollector.js'
|
|
27
|
+
import { ZonesCollector } from './ZonesCollector.js'
|
|
28
|
+
import { AuthCollector } from './AuthCollector.js'
|
|
29
|
+
import { EntitiesCollector } from './EntitiesCollector.js'
|
|
30
|
+
import DebugBar from './components/DebugBar.vue'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Symbol for debug bridge injection key
|
|
34
|
+
*/
|
|
35
|
+
export const DEBUG_BRIDGE_KEY = Symbol('debugBridge')
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Debug zone name for the DebugBar component
|
|
39
|
+
* Prefixed with _ to hide from ZonesCollector (internal zone)
|
|
40
|
+
*/
|
|
41
|
+
export const DEBUG_ZONE = '_app:debug'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Global DebugBar wrapper component
|
|
45
|
+
* Auto-injects the debug bridge - use in App.vue for app-wide debug bar
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* <!-- In App.vue template -->
|
|
49
|
+
* <router-view />
|
|
50
|
+
* <QdadmDebugBar />
|
|
51
|
+
*/
|
|
52
|
+
export const QdadmDebugBar = defineComponent({
|
|
53
|
+
name: 'QdadmDebugBar',
|
|
54
|
+
setup() {
|
|
55
|
+
const bridge = inject(DEBUG_BRIDGE_KEY, null)
|
|
56
|
+
return () => bridge ? h(DebugBar, { bridge }) : null
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* DebugModule - Integrates debug tools into the application
|
|
62
|
+
*/
|
|
63
|
+
export class DebugModule extends Module {
|
|
64
|
+
/**
|
|
65
|
+
* Module identifier
|
|
66
|
+
* @type {string}
|
|
67
|
+
*/
|
|
68
|
+
static name = 'debug'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* No dependencies - debug module should work standalone
|
|
72
|
+
* @type {string[]}
|
|
73
|
+
*/
|
|
74
|
+
static requires = []
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Very low priority - runs last after all modules are connected
|
|
78
|
+
* This ensures all signals and routes are registered before debug tools start
|
|
79
|
+
* @type {number}
|
|
80
|
+
*/
|
|
81
|
+
static priority = 1000
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a new DebugModule
|
|
85
|
+
*
|
|
86
|
+
* @param {object} [options={}] - Module options
|
|
87
|
+
* @param {boolean} [options.enabled=false] - Initial enabled state for collectors
|
|
88
|
+
* @param {number} [options.maxEntries=100] - Max entries per collector
|
|
89
|
+
* @param {boolean} [options.errorCollector=true] - Include ErrorCollector
|
|
90
|
+
* @param {boolean} [options.signalCollector=true] - Include SignalCollector
|
|
91
|
+
* @param {boolean} [options.toastCollector=true] - Include ToastCollector
|
|
92
|
+
* @param {boolean} [options.zonesCollector=true] - Include ZonesCollector
|
|
93
|
+
* @param {boolean} [options.authCollector=true] - Include AuthCollector
|
|
94
|
+
* @param {boolean} [options.entitiesCollector=true] - Include EntitiesCollector
|
|
95
|
+
*/
|
|
96
|
+
constructor(options = {}) {
|
|
97
|
+
super(options)
|
|
98
|
+
this._bridge = null
|
|
99
|
+
this._blockId = 'debug-bar'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if module should be enabled
|
|
104
|
+
*
|
|
105
|
+
* Debug module is enabled when:
|
|
106
|
+
* - In development mode (ctx.isDev)
|
|
107
|
+
* - OR debug option is explicitly true (ctx.debug)
|
|
108
|
+
*
|
|
109
|
+
* @param {import('../kernel/KernelContext.js').KernelContext} ctx
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
enabled(ctx) {
|
|
113
|
+
return ctx.isDev || ctx.debug
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Connect module to kernel
|
|
118
|
+
*
|
|
119
|
+
* Creates DebugBridge, registers collectors, and sets up DebugBar zone.
|
|
120
|
+
*
|
|
121
|
+
* @param {import('../kernel/KernelContext.js').KernelContext} ctx
|
|
122
|
+
*/
|
|
123
|
+
async connect(ctx) {
|
|
124
|
+
this.ctx = ctx
|
|
125
|
+
|
|
126
|
+
// Create debug bridge with options
|
|
127
|
+
this._bridge = createDebugBridge({
|
|
128
|
+
enabled: this.options.enabled ?? false
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Register default collectors
|
|
132
|
+
const collectorOptions = { maxEntries: this.options.maxEntries ?? 100 }
|
|
133
|
+
|
|
134
|
+
if (this.options.errorCollector !== false) {
|
|
135
|
+
this._bridge.addCollector(new ErrorCollector(collectorOptions))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.options.signalCollector !== false) {
|
|
139
|
+
this._bridge.addCollector(new SignalCollector(collectorOptions))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (this.options.toastCollector !== false) {
|
|
143
|
+
this._bridge.addCollector(new ToastCollector(collectorOptions))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.options.zonesCollector !== false) {
|
|
147
|
+
this._bridge.addCollector(new ZonesCollector(collectorOptions))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.options.authCollector !== false) {
|
|
151
|
+
this._bridge.addCollector(new AuthCollector(collectorOptions))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this.options.entitiesCollector !== false) {
|
|
155
|
+
this._bridge.addCollector(new EntitiesCollector(collectorOptions))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Install collectors with context
|
|
159
|
+
this._bridge.install(ctx)
|
|
160
|
+
|
|
161
|
+
// Define the debug zone (for backwards compatibility, but don't render in it)
|
|
162
|
+
// When using debugBar shorthand in Kernel, the root wrapper handles rendering.
|
|
163
|
+
// The zone is still defined so apps can use <QdadmZone name="app:debug" />
|
|
164
|
+
// if they're not using the debugBar shorthand.
|
|
165
|
+
ctx.zone(DEBUG_ZONE)
|
|
166
|
+
|
|
167
|
+
// Only register zone block if NOT using Kernel's root wrapper approach
|
|
168
|
+
// This prevents double-rendering when debugBar shorthand is used.
|
|
169
|
+
// Note: When debugBar shorthand is used, Kernel wraps root with QdadmDebugBar.
|
|
170
|
+
// If we also register in zone, layouts with <Zone name="app:debug" /> would render twice.
|
|
171
|
+
if (!this.options._kernelManaged) {
|
|
172
|
+
ctx.block(DEBUG_ZONE, {
|
|
173
|
+
id: this._blockId,
|
|
174
|
+
component: DebugBar,
|
|
175
|
+
props: {
|
|
176
|
+
bridge: this._bridge
|
|
177
|
+
},
|
|
178
|
+
weight: 100
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Provide debug bridge for injection
|
|
183
|
+
ctx.provide(DEBUG_BRIDGE_KEY, this._bridge)
|
|
184
|
+
|
|
185
|
+
// Register global component for use in App.vue (outside authenticated routes)
|
|
186
|
+
ctx.component('QdadmDebugBar', QdadmDebugBar)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Disconnect module from kernel
|
|
191
|
+
*
|
|
192
|
+
* Cleans up debug bridge and removes DebugBar from zone.
|
|
193
|
+
*/
|
|
194
|
+
async disconnect() {
|
|
195
|
+
// Uninstall bridge and all collectors
|
|
196
|
+
if (this._bridge) {
|
|
197
|
+
this._bridge.uninstall()
|
|
198
|
+
this._bridge = null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Call parent to clean up signal listeners
|
|
202
|
+
await super.disconnect()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get the debug bridge instance
|
|
207
|
+
*
|
|
208
|
+
* @returns {import('./DebugBridge.js').DebugBridge|null}
|
|
209
|
+
*/
|
|
210
|
+
getBridge() {
|
|
211
|
+
return this._bridge
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default DebugModule
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EntitiesCollector - Debug collector for Entity Managers
|
|
3
|
+
*
|
|
4
|
+
* This collector displays registered entity managers and their state:
|
|
5
|
+
* - All registered managers from Orchestrator
|
|
6
|
+
* - Storage type and capabilities
|
|
7
|
+
* - Cache status (enabled, valid, items, threshold)
|
|
8
|
+
* - Permissions (readOnly, canCreate, canUpdate, canDelete)
|
|
9
|
+
* - Relations (parent, children, parents)
|
|
10
|
+
*
|
|
11
|
+
* Shows current state rather than historical events.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const collector = new EntitiesCollector()
|
|
15
|
+
* collector.install(ctx)
|
|
16
|
+
* collector.getEntries() // [{ name: 'books', storage: 'ApiStorage', ... }, ...]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Collector } from './Collector.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Collector for Entity Manager state visualization
|
|
23
|
+
*/
|
|
24
|
+
export class EntitiesCollector extends Collector {
|
|
25
|
+
/**
|
|
26
|
+
* Collector name identifier
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
static name = 'entities'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This collector shows state, not events
|
|
33
|
+
* @type {boolean}
|
|
34
|
+
*/
|
|
35
|
+
static records = false
|
|
36
|
+
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
super(options)
|
|
39
|
+
this._ctx = null
|
|
40
|
+
this._signalCleanups = []
|
|
41
|
+
this._lastUpdate = 0
|
|
42
|
+
// Activity tracking: store previous stats to detect changes
|
|
43
|
+
this._previousStats = new Map() // entityName -> stats snapshot
|
|
44
|
+
this._activeEntities = new Set() // entities with unseen activity
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Internal install - store context reference and subscribe to signals
|
|
49
|
+
* The orchestrator is accessed lazily since it may not exist at install time.
|
|
50
|
+
* @param {object} ctx - Context object
|
|
51
|
+
* @protected
|
|
52
|
+
*/
|
|
53
|
+
_doInstall(ctx) {
|
|
54
|
+
this._ctx = ctx
|
|
55
|
+
|
|
56
|
+
// Subscribe to entity signals for reactive updates
|
|
57
|
+
// Uses deferred access since signals may not be ready at install time
|
|
58
|
+
this._setupSignals()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Setup signal listeners (deferred until signals available)
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_setupSignals() {
|
|
66
|
+
const signals = this._ctx?.signals
|
|
67
|
+
if (!signals) {
|
|
68
|
+
// Retry on next tick if signals not ready
|
|
69
|
+
setTimeout(() => this._setupSignals(), 100)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Listen to all entity lifecycle events
|
|
74
|
+
const entityCleanup = signals.on('entity:*', () => {
|
|
75
|
+
this._lastUpdate = Date.now()
|
|
76
|
+
this.notifyChange()
|
|
77
|
+
})
|
|
78
|
+
this._signalCleanups.push(entityCleanup)
|
|
79
|
+
|
|
80
|
+
// Listen to cache invalidation
|
|
81
|
+
const cacheCleanup = signals.on('cache:entity:invalidated', () => {
|
|
82
|
+
this._lastUpdate = Date.now()
|
|
83
|
+
this.notifyChange()
|
|
84
|
+
})
|
|
85
|
+
this._signalCleanups.push(cacheCleanup)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Internal uninstall - cleanup signal subscriptions
|
|
90
|
+
* @protected
|
|
91
|
+
*/
|
|
92
|
+
_doUninstall() {
|
|
93
|
+
// Cleanup signal listeners
|
|
94
|
+
for (const cleanup of this._signalCleanups) {
|
|
95
|
+
if (typeof cleanup === 'function') cleanup()
|
|
96
|
+
}
|
|
97
|
+
this._signalCleanups = []
|
|
98
|
+
this._ctx = null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get orchestrator lazily (may not exist at install time)
|
|
103
|
+
* @returns {object|null}
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
get _orchestrator() {
|
|
107
|
+
return this._ctx?.orchestrator ?? null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get badge - show count of entities with unseen activity
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
getBadge() {
|
|
115
|
+
if (!this._orchestrator) return 0
|
|
116
|
+
// Check for new activity before returning badge count
|
|
117
|
+
this._detectActivity()
|
|
118
|
+
return this._activeEntities.size
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Detect activity by comparing current stats with previous snapshot
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
_detectActivity() {
|
|
126
|
+
if (!this._orchestrator) return
|
|
127
|
+
|
|
128
|
+
for (const name of this._orchestrator.getRegisteredNames()) {
|
|
129
|
+
try {
|
|
130
|
+
const manager = this._orchestrator.get(name)
|
|
131
|
+
const currentStats = manager.getStats?.()
|
|
132
|
+
if (!currentStats) continue
|
|
133
|
+
|
|
134
|
+
const prevStats = this._previousStats.get(name)
|
|
135
|
+
if (!prevStats) {
|
|
136
|
+
// First time seeing this entity, store snapshot
|
|
137
|
+
this._previousStats.set(name, { ...currentStats })
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if any stat changed
|
|
142
|
+
const hasChanged =
|
|
143
|
+
currentStats.list !== prevStats.list ||
|
|
144
|
+
currentStats.get !== prevStats.get ||
|
|
145
|
+
currentStats.create !== prevStats.create ||
|
|
146
|
+
currentStats.update !== prevStats.update ||
|
|
147
|
+
currentStats.delete !== prevStats.delete ||
|
|
148
|
+
currentStats.cacheHits !== prevStats.cacheHits ||
|
|
149
|
+
currentStats.cacheMisses !== prevStats.cacheMisses
|
|
150
|
+
|
|
151
|
+
if (hasChanged) {
|
|
152
|
+
this._activeEntities.add(name)
|
|
153
|
+
// Update snapshot for next comparison
|
|
154
|
+
this._previousStats.set(name, { ...currentStats })
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// Skip failed managers
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get total count - number of registered entities
|
|
164
|
+
* @returns {number}
|
|
165
|
+
*/
|
|
166
|
+
getTotalCount() {
|
|
167
|
+
if (!this._orchestrator) return 0
|
|
168
|
+
return this._orchestrator.getRegisteredNames().length
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get all entity information for display
|
|
173
|
+
* @returns {Array<object>} Entity info array
|
|
174
|
+
*/
|
|
175
|
+
getEntries() {
|
|
176
|
+
if (!this._orchestrator) {
|
|
177
|
+
return [{ type: 'status', message: 'No orchestrator configured' }]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const names = this._orchestrator.getRegisteredNames()
|
|
181
|
+
if (names.length === 0) {
|
|
182
|
+
return [{ type: 'status', message: 'No entities registered' }]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const entries = []
|
|
186
|
+
|
|
187
|
+
for (const name of names.sort()) {
|
|
188
|
+
try {
|
|
189
|
+
const manager = this._orchestrator.get(name)
|
|
190
|
+
entries.push(this._buildEntityInfo(name, manager))
|
|
191
|
+
} catch (e) {
|
|
192
|
+
entries.push({
|
|
193
|
+
name,
|
|
194
|
+
error: e.message
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return entries
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build entity info object from manager
|
|
204
|
+
* @param {string} name - Entity name
|
|
205
|
+
* @param {EntityManager} manager - Manager instance
|
|
206
|
+
* @returns {object} Entity info
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_buildEntityInfo(name, manager) {
|
|
210
|
+
const cache = manager.getCacheInfo?.() || {}
|
|
211
|
+
const storage = manager.storage
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
name,
|
|
215
|
+
hasActivity: this._activeEntities.has(name),
|
|
216
|
+
label: manager.label,
|
|
217
|
+
labelPlural: manager.labelPlural,
|
|
218
|
+
routePrefix: manager.routePrefix,
|
|
219
|
+
idField: manager.idField,
|
|
220
|
+
|
|
221
|
+
// Storage info
|
|
222
|
+
storage: {
|
|
223
|
+
type: storage?.constructor?.name || 'None',
|
|
224
|
+
endpoint: storage?.endpoint || storage?._endpoint || null,
|
|
225
|
+
capabilities: storage?.constructor?.capabilities || {}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// Cache info
|
|
229
|
+
cache: {
|
|
230
|
+
enabled: cache.enabled ?? false,
|
|
231
|
+
valid: cache.valid ?? false,
|
|
232
|
+
itemCount: cache.itemCount ?? 0,
|
|
233
|
+
total: cache.total ?? 0,
|
|
234
|
+
threshold: cache.threshold ?? 0,
|
|
235
|
+
overflow: cache.overflow ?? false,
|
|
236
|
+
loadedAt: cache.loadedAt ? new Date(cache.loadedAt).toLocaleTimeString() : null,
|
|
237
|
+
// Include cached items for inspection (limited to first 50)
|
|
238
|
+
items: this._getCacheItems(manager, 50)
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Permissions
|
|
242
|
+
permissions: {
|
|
243
|
+
readOnly: manager.readOnly,
|
|
244
|
+
canCreate: manager.canCreate?.() ?? true,
|
|
245
|
+
canUpdate: manager.canUpdate?.() ?? true,
|
|
246
|
+
canDelete: manager.canDelete?.() ?? true,
|
|
247
|
+
canList: manager.canList?.() ?? true
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// Warmup
|
|
251
|
+
warmup: {
|
|
252
|
+
enabled: manager.warmupEnabled ?? false
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Operation stats
|
|
256
|
+
stats: manager.getStats?.() || {
|
|
257
|
+
list: 0,
|
|
258
|
+
get: 0,
|
|
259
|
+
create: 0,
|
|
260
|
+
update: 0,
|
|
261
|
+
delete: 0,
|
|
262
|
+
cacheHits: 0,
|
|
263
|
+
cacheMisses: 0,
|
|
264
|
+
maxItemsSeen: 0,
|
|
265
|
+
maxTotal: 0
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// Fields
|
|
269
|
+
fields: {
|
|
270
|
+
count: Object.keys(manager.fields || {}).length,
|
|
271
|
+
names: Object.keys(manager.fields || {}).slice(0, 10), // First 10
|
|
272
|
+
required: manager.getRequiredFields?.() || []
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
// Relations
|
|
276
|
+
relations: {
|
|
277
|
+
parent: manager._parent ? {
|
|
278
|
+
entity: manager._parent.entity,
|
|
279
|
+
foreignKey: manager._parent.foreignKey
|
|
280
|
+
} : null,
|
|
281
|
+
parents: Object.entries(manager._parents || {}).map(([key, config]) => ({
|
|
282
|
+
key,
|
|
283
|
+
entity: config.entity,
|
|
284
|
+
foreignKey: config.foreignKey
|
|
285
|
+
})),
|
|
286
|
+
children: Object.entries(manager._children || {}).map(([key, config]) => ({
|
|
287
|
+
key,
|
|
288
|
+
entity: config.entity,
|
|
289
|
+
endpoint: config.endpoint || null
|
|
290
|
+
}))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get cached items from a manager
|
|
297
|
+
* @param {EntityManager} manager - Manager instance
|
|
298
|
+
* @param {number} limit - Max items to return
|
|
299
|
+
* @returns {Array} Cached items (limited)
|
|
300
|
+
* @private
|
|
301
|
+
*/
|
|
302
|
+
_getCacheItems(manager, limit = 50) {
|
|
303
|
+
try {
|
|
304
|
+
// Try different cache access methods
|
|
305
|
+
if (manager._cache && Array.isArray(manager._cache)) {
|
|
306
|
+
return manager._cache.slice(0, limit)
|
|
307
|
+
}
|
|
308
|
+
if (manager._cacheMap && manager._cacheMap instanceof Map) {
|
|
309
|
+
return Array.from(manager._cacheMap.values()).slice(0, limit)
|
|
310
|
+
}
|
|
311
|
+
if (manager.cache && Array.isArray(manager.cache)) {
|
|
312
|
+
return manager.cache.slice(0, limit)
|
|
313
|
+
}
|
|
314
|
+
// Try getAll if available and cache is valid
|
|
315
|
+
if (manager.getCacheInfo?.()?.valid && manager._cachedItems) {
|
|
316
|
+
return manager._cachedItems.slice(0, limit)
|
|
317
|
+
}
|
|
318
|
+
return []
|
|
319
|
+
} catch (e) {
|
|
320
|
+
return []
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Force refresh a specific entity's cache
|
|
326
|
+
* @param {string} entityName - Entity name
|
|
327
|
+
* @param {boolean} [reload=false] - If true, reload cache after invalidation
|
|
328
|
+
* @returns {Promise<boolean>} True if cache refreshed
|
|
329
|
+
*/
|
|
330
|
+
async refreshCache(entityName, reload = false) {
|
|
331
|
+
if (!this._orchestrator) return false
|
|
332
|
+
try {
|
|
333
|
+
const manager = this._orchestrator.get(entityName)
|
|
334
|
+
manager.invalidateCache()
|
|
335
|
+
if (reload) {
|
|
336
|
+
await manager.ensureCache()
|
|
337
|
+
}
|
|
338
|
+
this.notifyChange()
|
|
339
|
+
return true
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.error('[EntitiesCollector] Failed to refresh cache:', e)
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Invalidate a specific entity's cache
|
|
348
|
+
* @param {string} entityName - Entity name
|
|
349
|
+
*/
|
|
350
|
+
invalidateCache(entityName) {
|
|
351
|
+
if (!this._orchestrator) return
|
|
352
|
+
try {
|
|
353
|
+
const manager = this._orchestrator.get(entityName)
|
|
354
|
+
manager.invalidateCache()
|
|
355
|
+
} catch (e) {
|
|
356
|
+
console.error('[EntitiesCollector] Failed to invalidate cache:', e)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Mark all entities as seen (clear activity state)
|
|
362
|
+
* Call this when the panel is viewed
|
|
363
|
+
* Note: Does not call notifyChange() to avoid re-render loop
|
|
364
|
+
*/
|
|
365
|
+
markSeen() {
|
|
366
|
+
this._activeEntities.clear()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Mark a specific entity as seen
|
|
371
|
+
* @param {string} entityName - Entity name
|
|
372
|
+
*/
|
|
373
|
+
markEntitySeen(entityName) {
|
|
374
|
+
this._activeEntities.delete(entityName)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorCollector - Captures JavaScript errors and unhandled promise rejections
|
|
3
|
+
*
|
|
4
|
+
* This collector listens to global window error events and unhandled promise
|
|
5
|
+
* rejections, recording them for display in the debug panel.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const collector = new ErrorCollector()
|
|
9
|
+
* collector.install(ctx)
|
|
10
|
+
* // Errors are now automatically recorded
|
|
11
|
+
* // Later...
|
|
12
|
+
* collector.uninstall()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Collector } from './Collector.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collector for JavaScript errors and unhandled promise rejections
|
|
19
|
+
*/
|
|
20
|
+
export class ErrorCollector extends Collector {
|
|
21
|
+
/**
|
|
22
|
+
* Collector name identifier
|
|
23
|
+
* @type {string}
|
|
24
|
+
*/
|
|
25
|
+
static name = 'errors'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Internal install - subscribe to error and unhandledrejection events
|
|
29
|
+
* @param {object} ctx - Context object (not used for error collection)
|
|
30
|
+
* @protected
|
|
31
|
+
*/
|
|
32
|
+
_doInstall(ctx) {
|
|
33
|
+
this._handler = (event) => {
|
|
34
|
+
this.record({
|
|
35
|
+
message: event.message,
|
|
36
|
+
filename: event.filename,
|
|
37
|
+
lineno: event.lineno,
|
|
38
|
+
colno: event.colno,
|
|
39
|
+
error: event.error?.stack
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
this._rejectionHandler = (event) => {
|
|
43
|
+
this.record({
|
|
44
|
+
message: 'Unhandled Promise Rejection',
|
|
45
|
+
reason: String(event.reason)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
window.addEventListener('error', this._handler)
|
|
49
|
+
window.addEventListener('unhandledrejection', this._rejectionHandler)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal uninstall - remove event listeners
|
|
54
|
+
* @protected
|
|
55
|
+
*/
|
|
56
|
+
_doUninstall() {
|
|
57
|
+
if (this._handler) {
|
|
58
|
+
window.removeEventListener('error', this._handler)
|
|
59
|
+
this._handler = null
|
|
60
|
+
}
|
|
61
|
+
if (this._rejectionHandler) {
|
|
62
|
+
window.removeEventListener('unhandledrejection', this._rejectionHandler)
|
|
63
|
+
this._rejectionHandler = null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|