@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,839 @@
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
+
14
+ import {
15
+ Collector,
16
+ type CollectorContext,
17
+ type CollectorEntry,
18
+ type CollectorManifest,
19
+ type CollectorOptions,
20
+ type CollectorSnapshot,
21
+ } from './Collector'
22
+ import type { EntityManager } from '../../entity/EntityManager'
23
+
24
+ /**
25
+ * Storage info structure
26
+ */
27
+ export interface StorageInfo {
28
+ type: string
29
+ endpoint: string | null
30
+ capabilities: Record<string, unknown>
31
+ hasNormalize: boolean
32
+ hasDenormalize: boolean
33
+ }
34
+
35
+ /**
36
+ * Multi-storage entry
37
+ */
38
+ export interface MultiStorageEntry {
39
+ name: string
40
+ type: string
41
+ endpoint: string | null
42
+ hasNormalize: boolean
43
+ hasDenormalize: boolean
44
+ capabilities: Record<string, unknown>
45
+ }
46
+
47
+ /**
48
+ * Multi-storage info
49
+ */
50
+ export interface MultiStorageInfo {
51
+ enabled: boolean
52
+ storages: MultiStorageEntry[]
53
+ }
54
+
55
+ /**
56
+ * Cache info structure
57
+ */
58
+ export interface DetailCacheInfo {
59
+ enabled: boolean
60
+ ttlMs: number
61
+ size: number
62
+ maxSize: number
63
+ }
64
+
65
+ export interface CacheInfo {
66
+ enabled: boolean
67
+ valid: boolean
68
+ itemCount: number
69
+ total: number
70
+ threshold: number
71
+ overflow: boolean
72
+ loadedAt: string | null
73
+ items: unknown[]
74
+ /** TTL in milliseconds (0=disabled, -1=infinite, >0=TTL) */
75
+ ttlMs: number
76
+ /** Timestamp when cache expires (null if no TTL or infinite) */
77
+ expiresAt: number | null
78
+ /** Whether the cache has expired based on TTL */
79
+ expired: boolean
80
+ /** Whether the entity uses asymmetric mode */
81
+ asymmetric: boolean
82
+ /** Detail cache info (only when asymmetric + detail cache enabled) */
83
+ detailCache: DetailCacheInfo | null
84
+ }
85
+
86
+ /**
87
+ * Permissions info
88
+ */
89
+ export interface PermissionsInfo {
90
+ readOnly: boolean
91
+ canCreate: boolean
92
+ canUpdate: boolean
93
+ canDelete: boolean
94
+ canList: boolean
95
+ }
96
+
97
+ /**
98
+ * Warmup info
99
+ */
100
+ export interface WarmupInfo {
101
+ enabled: boolean
102
+ }
103
+
104
+ /**
105
+ * Stats info
106
+ */
107
+ export interface StatsInfo {
108
+ list: number
109
+ get: number
110
+ create: number
111
+ update: number
112
+ delete: number
113
+ cacheHits: number
114
+ cacheMisses: number
115
+ detailCacheHits: number
116
+ detailCacheMisses: number
117
+ maxItemsSeen: number
118
+ maxTotal: number
119
+ }
120
+
121
+ /**
122
+ * Fields info
123
+ */
124
+ export interface FieldsInfo {
125
+ count: number
126
+ names: string[]
127
+ required: string[]
128
+ }
129
+
130
+ /**
131
+ * Relation parent info
132
+ */
133
+ export interface RelationParentInfo {
134
+ entity: string
135
+ foreignKey: string
136
+ }
137
+
138
+ /**
139
+ * Relation parents entry
140
+ */
141
+ export interface RelationParentsEntry {
142
+ key: string
143
+ entity: string
144
+ foreignKey: string
145
+ }
146
+
147
+ /**
148
+ * Relation children entry
149
+ */
150
+ export interface RelationChildrenEntry {
151
+ key: string
152
+ entity: string
153
+ endpoint: string | null
154
+ }
155
+
156
+ /**
157
+ * Field reference entry
158
+ */
159
+ export interface FieldReferenceEntry {
160
+ field: string
161
+ entity: string
162
+ }
163
+
164
+ /**
165
+ * Relations info
166
+ */
167
+ export interface RelationsInfo {
168
+ parent: RelationParentInfo | null
169
+ parents: RelationParentsEntry[]
170
+ children: RelationChildrenEntry[]
171
+ references: FieldReferenceEntry[]
172
+ }
173
+
174
+ /**
175
+ * Entity entry for display
176
+ */
177
+ export interface EntityEntry extends CollectorEntry {
178
+ name?: string
179
+ system?: boolean
180
+ hasActivity?: boolean
181
+ label?: string
182
+ labelPlural?: string
183
+ routePrefix?: string
184
+ idField?: string
185
+ storage?: StorageInfo
186
+ multiStorage?: MultiStorageInfo | null
187
+ cache?: CacheInfo
188
+ permissions?: PermissionsInfo
189
+ authSensitive?: boolean
190
+ warmup?: WarmupInfo
191
+ stats?: StatsInfo
192
+ fields?: FieldsInfo
193
+ relations?: RelationsInfo
194
+ type?: 'status'
195
+ message?: string
196
+ error?: string
197
+ }
198
+
199
+ /**
200
+ * Orchestrator interface for entity operations
201
+ */
202
+ interface EntityOrchestrator {
203
+ getRegisteredNames(): string[]
204
+ get(name: string): EntityManager
205
+ }
206
+
207
+ /**
208
+ * Collector for Entity Manager state visualization
209
+ */
210
+ export class EntitiesCollector extends Collector<EntityEntry> {
211
+ static override collectorName = 'entities'
212
+ static override records = false
213
+
214
+ private _signalCleanups: Array<() => void> = []
215
+ private _lastUpdate = 0
216
+ private _previousStats: Map<string, StatsInfo> = new Map()
217
+ private _activeEntities: Set<string> = new Set()
218
+
219
+ constructor(options: CollectorOptions = {}) {
220
+ super(options)
221
+ }
222
+
223
+ /**
224
+ * Internal install - store context reference and subscribe to signals
225
+ */
226
+ protected override _doInstall(ctx: CollectorContext): void {
227
+ this._ctx = ctx
228
+ this._setupSignals()
229
+ }
230
+
231
+ /**
232
+ * Setup signal listeners (deferred until signals available)
233
+ */
234
+ private _setupSignals(): void {
235
+ const signals = this._ctx?.signals
236
+ if (!signals) {
237
+ setTimeout(() => this._setupSignals(), 100)
238
+ return
239
+ }
240
+
241
+ // Listen to all entity lifecycle events
242
+ const entityCleanup = signals.on('entity:*', () => {
243
+ this._lastUpdate = Date.now()
244
+ this.notifyChange()
245
+ })
246
+ this._signalCleanups.push(entityCleanup)
247
+
248
+ // Listen to entity data changes (CRUD operations)
249
+ const dataCleanup = signals.on('entity:data-invalidate', () => {
250
+ this._lastUpdate = Date.now()
251
+ this.notifyChange()
252
+ })
253
+ this._signalCleanups.push(dataCleanup)
254
+ }
255
+
256
+ /**
257
+ * Internal uninstall - cleanup signal subscriptions
258
+ */
259
+ protected override _doUninstall(): void {
260
+ for (const cleanup of this._signalCleanups) {
261
+ if (typeof cleanup === 'function') cleanup()
262
+ }
263
+ this._signalCleanups = []
264
+ this._ctx = null
265
+ }
266
+
267
+ /**
268
+ * Get orchestrator lazily (may not exist at install time)
269
+ */
270
+ private get _orchestrator(): EntityOrchestrator | null {
271
+ return (this._ctx as CollectorContext & { orchestrator?: EntityOrchestrator })?.orchestrator ?? null
272
+ }
273
+
274
+ /**
275
+ * Get badge - show count of entities with unseen activity
276
+ */
277
+ override getBadge(): number {
278
+ if (!this._orchestrator) return 0
279
+ this._detectActivity()
280
+ return this._activeEntities.size
281
+ }
282
+
283
+ /**
284
+ * Detect activity by comparing current stats with previous snapshot
285
+ */
286
+ private _detectActivity(): void {
287
+ if (!this._orchestrator) return
288
+
289
+ for (const name of this._orchestrator.getRegisteredNames()) {
290
+ try {
291
+ const manager = this._orchestrator.get(name) as EntityManager & { getStats?: () => StatsInfo }
292
+ const currentStats = manager.getStats?.()
293
+ if (!currentStats) continue
294
+
295
+ const prevStats = this._previousStats.get(name)
296
+ if (!prevStats) {
297
+ this._previousStats.set(name, { ...currentStats })
298
+ continue
299
+ }
300
+
301
+ const hasChanged =
302
+ currentStats.list !== prevStats.list ||
303
+ currentStats.get !== prevStats.get ||
304
+ currentStats.create !== prevStats.create ||
305
+ currentStats.update !== prevStats.update ||
306
+ currentStats.delete !== prevStats.delete ||
307
+ currentStats.cacheHits !== prevStats.cacheHits ||
308
+ currentStats.cacheMisses !== prevStats.cacheMisses ||
309
+ currentStats.detailCacheHits !== prevStats.detailCacheHits ||
310
+ currentStats.detailCacheMisses !== prevStats.detailCacheMisses
311
+
312
+ if (hasChanged) {
313
+ this._activeEntities.add(name)
314
+ this._previousStats.set(name, { ...currentStats })
315
+ }
316
+ } catch {
317
+ // Skip failed managers
318
+ }
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Get total count - number of registered entities
324
+ */
325
+ getTotalCount(): number {
326
+ if (!this._orchestrator) return 0
327
+ return this._orchestrator.getRegisteredNames().length
328
+ }
329
+
330
+ /**
331
+ * Get all entity information for display
332
+ */
333
+ override getEntries(): EntityEntry[] {
334
+ if (!this._orchestrator) {
335
+ return [{ timestamp: Date.now(), type: 'status', message: 'No orchestrator configured' }]
336
+ }
337
+
338
+ const names = this._orchestrator.getRegisteredNames()
339
+ if (names.length === 0) {
340
+ return [{ timestamp: Date.now(), type: 'status', message: 'No entities registered' }]
341
+ }
342
+
343
+ const entries: EntityEntry[] = []
344
+
345
+ for (const name of names.sort()) {
346
+ try {
347
+ const manager = this._orchestrator.get(name)
348
+ entries.push(this._buildEntityInfo(name, manager))
349
+ } catch (e) {
350
+ entries.push({
351
+ timestamp: Date.now(),
352
+ name,
353
+ error: (e as Error).message
354
+ })
355
+ }
356
+ }
357
+
358
+ // Sort: system entities first, then alphabetically
359
+ entries.sort((a, b) => {
360
+ if (a.system && !b.system) return -1
361
+ if (!a.system && b.system) return 1
362
+ return (a.name || '').localeCompare(b.name || '')
363
+ })
364
+
365
+ return entries
366
+ }
367
+
368
+ /**
369
+ * Build entity info object from manager
370
+ */
371
+ private _buildEntityInfo(name: string, manager: EntityManager): EntityEntry {
372
+ const extManager = manager as EntityManager & {
373
+ getCacheInfo?: () => {
374
+ enabled?: boolean
375
+ valid?: boolean
376
+ itemCount?: number
377
+ total?: number
378
+ threshold?: number
379
+ overflow?: boolean
380
+ loadedAt?: number
381
+ ttlMs?: number
382
+ expiresAt?: number | null
383
+ expired?: boolean
384
+ asymmetric?: boolean
385
+ detailCache?: { enabled: boolean; ttlMs: number; size: number; maxSize: number } | null
386
+ }
387
+ getStats?: () => StatsInfo
388
+ getRequiredFields?: () => string[]
389
+ system?: boolean
390
+ label?: string
391
+ labelPlural?: string
392
+ routePrefix?: string
393
+ idField?: string
394
+ readOnly?: boolean
395
+ canCreate?: () => boolean
396
+ canUpdate?: () => boolean
397
+ canDelete?: () => boolean
398
+ canList?: () => boolean
399
+ _authSensitive?: boolean
400
+ warmupEnabled?: boolean
401
+ fields?: Record<string, { reference?: { entity?: string } }>
402
+ _parent?: { entity: string; foreignKey: string }
403
+ _parents?: Record<string, { entity: string; foreignKey: string }>
404
+ _children?: Record<string, { entity: string; endpoint?: string }>
405
+ _cache?: unknown[]
406
+ _cacheMap?: Map<unknown, unknown>
407
+ cache?: unknown[]
408
+ _cachedItems?: unknown[]
409
+ resolveStorage?: (method: string, context: unknown) => { storage: unknown }
410
+ }
411
+
412
+ const cache = extManager.getCacheInfo?.() || {}
413
+ const storage = extManager.storage as {
414
+ constructor?: { storageName?: string; name?: string; capabilities?: Record<string, unknown> }
415
+ endpoint?: string
416
+ _endpoint?: string
417
+ capabilities?: Record<string, unknown>
418
+ _normalize?: unknown
419
+ _denormalize?: unknown
420
+ } | null
421
+
422
+ return {
423
+ timestamp: Date.now(),
424
+ name,
425
+ system: extManager.system ?? false,
426
+ hasActivity: this._activeEntities.has(name),
427
+ label: extManager.label,
428
+ labelPlural: extManager.labelPlural,
429
+ routePrefix: extManager.routePrefix,
430
+ idField: extManager.idField,
431
+
432
+ storage: {
433
+ type: storage?.constructor?.storageName || storage?.constructor?.name || 'None',
434
+ endpoint: storage?.endpoint || storage?._endpoint || null,
435
+ capabilities: storage?.capabilities || storage?.constructor?.capabilities || {},
436
+ hasNormalize: !!(storage?._normalize),
437
+ hasDenormalize: !!(storage?._denormalize)
438
+ },
439
+
440
+ multiStorage: this._detectMultiStorage(extManager),
441
+
442
+ cache: {
443
+ enabled: cache.enabled ?? false,
444
+ valid: cache.valid ?? false,
445
+ itemCount: cache.itemCount ?? 0,
446
+ total: cache.total ?? 0,
447
+ threshold: cache.threshold ?? 0,
448
+ overflow: cache.overflow ?? false,
449
+ loadedAt: cache.loadedAt ? new Date(cache.loadedAt).toLocaleTimeString() : null,
450
+ items: this._getCacheItems(extManager, 50),
451
+ ttlMs: cache.ttlMs ?? -1,
452
+ expiresAt: cache.expiresAt ?? null,
453
+ expired: cache.expired ?? false,
454
+ asymmetric: cache.asymmetric ?? false,
455
+ detailCache: cache.detailCache ?? null,
456
+ },
457
+
458
+ permissions: {
459
+ readOnly: extManager.readOnly ?? false,
460
+ canCreate: extManager.canCreate?.() ?? true,
461
+ canUpdate: extManager.canUpdate?.() ?? true,
462
+ canDelete: extManager.canDelete?.() ?? true,
463
+ canList: extManager.canList?.() ?? true
464
+ },
465
+
466
+ authSensitive: extManager._authSensitive ?? false,
467
+
468
+ warmup: {
469
+ enabled: extManager.warmupEnabled ?? false
470
+ },
471
+
472
+ stats: extManager.getStats?.() || {
473
+ list: 0,
474
+ get: 0,
475
+ create: 0,
476
+ update: 0,
477
+ delete: 0,
478
+ cacheHits: 0,
479
+ cacheMisses: 0,
480
+ detailCacheHits: 0,
481
+ detailCacheMisses: 0,
482
+ maxItemsSeen: 0,
483
+ maxTotal: 0
484
+ },
485
+
486
+ fields: {
487
+ count: Object.keys(extManager.fields || {}).length,
488
+ names: Object.keys(extManager.fields || {}).slice(0, 10),
489
+ required: extManager.getRequiredFields?.() || []
490
+ },
491
+
492
+ relations: {
493
+ parent: extManager._parent ? {
494
+ entity: extManager._parent.entity,
495
+ foreignKey: extManager._parent.foreignKey
496
+ } : null,
497
+ parents: Object.entries(extManager._parents || {}).map(([key, config]) => ({
498
+ key,
499
+ entity: config.entity,
500
+ foreignKey: config.foreignKey
501
+ })),
502
+ children: Object.entries(extManager._children || {}).map(([key, config]) => ({
503
+ key,
504
+ entity: config.entity,
505
+ endpoint: config.endpoint || null
506
+ })),
507
+ references: Object.entries(extManager.fields || {})
508
+ .filter(([, field]) => field.reference?.entity)
509
+ .map(([fieldName, field]) => ({
510
+ field: fieldName,
511
+ entity: field.reference!.entity!
512
+ }))
513
+ }
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Detect if a manager uses multi-storage pattern
519
+ */
520
+ private _detectMultiStorage(manager: EntityManager): MultiStorageInfo | null {
521
+ const extManager = manager as EntityManager & {
522
+ resolveStorage?: (method: string, context: unknown) => { storage: unknown }
523
+ storage?: unknown
524
+ }
525
+
526
+ const hasCustomResolve = extManager.resolveStorage &&
527
+ extManager.resolveStorage.toString() !== 'resolveStorage(method, context) {\n return { storage: this.storage };\n }'
528
+
529
+ if (!hasCustomResolve) {
530
+ const additionalStorages = this._findAdditionalStorages(manager)
531
+ if (additionalStorages.length === 0) {
532
+ return null
533
+ }
534
+ }
535
+
536
+ const storages = this._findAdditionalStorages(manager)
537
+
538
+ return {
539
+ enabled: true,
540
+ storages: storages.map(s => ({
541
+ name: s.name,
542
+ type: s.storage?.constructor?.storageName || s.storage?.constructor?.name || 'Unknown',
543
+ endpoint: s.storage?.endpoint || null,
544
+ hasNormalize: !!(s.storage?._normalize),
545
+ hasDenormalize: !!(s.storage?._denormalize),
546
+ capabilities: s.storage?.capabilities || s.storage?.constructor?.capabilities || {}
547
+ }))
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Find additional storage instances on a manager
553
+ */
554
+ private _findAdditionalStorages(manager: EntityManager): Array<{
555
+ name: string
556
+ storage: {
557
+ constructor?: { storageName?: string; name?: string; capabilities?: Record<string, unknown> }
558
+ endpoint?: string
559
+ capabilities?: Record<string, unknown>
560
+ _normalize?: unknown
561
+ _denormalize?: unknown
562
+ list?: (params: unknown) => unknown
563
+ get?: (id: unknown) => unknown
564
+ fetch?: (params: unknown) => unknown
565
+ }
566
+ }> {
567
+ const storages: Array<{
568
+ name: string
569
+ storage: {
570
+ constructor?: { storageName?: string; name?: string; capabilities?: Record<string, unknown> }
571
+ endpoint?: string
572
+ capabilities?: Record<string, unknown>
573
+ _normalize?: unknown
574
+ _denormalize?: unknown
575
+ list?: (params: unknown) => unknown
576
+ get?: (id: unknown) => unknown
577
+ fetch?: (params: unknown) => unknown
578
+ }
579
+ }> = []
580
+ const extManager = manager as EntityManager & { storage?: unknown } & Record<string, unknown>
581
+ const mainStorage = extManager.storage
582
+
583
+ for (const key of Object.keys(extManager)) {
584
+ if (key.startsWith('_') || key === 'storage') continue
585
+
586
+ const value = extManager[key] as {
587
+ constructor?: { storageName?: string; name?: string; capabilities?: Record<string, unknown> }
588
+ endpoint?: string
589
+ capabilities?: Record<string, unknown>
590
+ _normalize?: unknown
591
+ _denormalize?: unknown
592
+ list?: (params: unknown) => unknown
593
+ get?: (id: unknown) => unknown
594
+ fetch?: (params: unknown) => unknown
595
+ } | null
596
+ if (!value || typeof value !== 'object') continue
597
+
598
+ const isStorage = (
599
+ (typeof value.list === 'function' && typeof value.get === 'function') ||
600
+ (value.endpoint && typeof value.fetch === 'function')
601
+ )
602
+
603
+ if (isStorage && value !== mainStorage) {
604
+ storages.push({ name: key, storage: value })
605
+ }
606
+ }
607
+
608
+ return storages
609
+ }
610
+
611
+ /**
612
+ * Get cached items from a manager
613
+ */
614
+ private _getCacheItems(manager: EntityManager, limit = 50): unknown[] {
615
+ const extManager = manager as EntityManager & {
616
+ _cache?: unknown[]
617
+ _cacheMap?: Map<unknown, unknown>
618
+ cache?: unknown[]
619
+ _cachedItems?: unknown[]
620
+ getCacheInfo?: () => { valid?: boolean }
621
+ }
622
+
623
+ try {
624
+ if (extManager._cache && Array.isArray(extManager._cache)) {
625
+ return extManager._cache.slice(0, limit)
626
+ }
627
+ if (extManager._cacheMap && extManager._cacheMap instanceof Map) {
628
+ return Array.from(extManager._cacheMap.values()).slice(0, limit)
629
+ }
630
+ if (extManager.cache && Array.isArray(extManager.cache)) {
631
+ return extManager.cache.slice(0, limit)
632
+ }
633
+ if (extManager.getCacheInfo?.()?.valid && extManager._cachedItems) {
634
+ return extManager._cachedItems.slice(0, limit)
635
+ }
636
+ return []
637
+ } catch {
638
+ return []
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Force refresh a specific entity's cache
644
+ */
645
+ async refreshCache(entityName: string, reload = false): Promise<boolean> {
646
+ if (!this._orchestrator) return false
647
+ try {
648
+ const manager = this._orchestrator.get(entityName) as EntityManager & {
649
+ invalidateCache: () => void
650
+ ensureCache: () => Promise<void>
651
+ }
652
+ manager.invalidateCache()
653
+ if (reload) {
654
+ await manager.ensureCache()
655
+ }
656
+ this.notifyChange()
657
+ return true
658
+ } catch (e) {
659
+ console.error('[EntitiesCollector] Failed to refresh cache:', e)
660
+ return false
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Invalidate a specific entity's cache
666
+ */
667
+ invalidateCache(entityName: string): void {
668
+ if (!this._orchestrator) return
669
+ try {
670
+ const manager = this._orchestrator.get(entityName) as EntityManager & {
671
+ invalidateCache: () => void
672
+ }
673
+ manager.invalidateCache()
674
+ } catch (e) {
675
+ console.error('[EntitiesCollector] Failed to invalidate cache:', e)
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Mark all entities as seen (clear activity state)
681
+ */
682
+ markSeen(): void {
683
+ this._activeEntities.clear()
684
+ }
685
+
686
+ /**
687
+ * Mark a specific entity as seen
688
+ */
689
+ markEntitySeen(entityName: string): void {
690
+ this._activeEntities.delete(entityName)
691
+ }
692
+
693
+ /**
694
+ * Test fetch data from a specific entity
695
+ */
696
+ async testFetch(entityName: string): Promise<{ success: boolean; count?: number; error?: string; status?: number }> {
697
+ if (!this._orchestrator) {
698
+ return { success: false, error: 'No orchestrator' }
699
+ }
700
+ try {
701
+ const manager = this._orchestrator.get(entityName) as EntityManager & {
702
+ storage: { list: (params: { page: number; page_size: number }) => Promise<{ total?: number; items?: unknown[] }> }
703
+ }
704
+ const result = await manager.storage.list({ page: 1, page_size: 1 })
705
+ return {
706
+ success: true,
707
+ count: result.total ?? result.items?.length ?? 0
708
+ }
709
+ } catch (e) {
710
+ return {
711
+ success: false,
712
+ error: (e as Error).message,
713
+ status: (e as Error & { status?: number }).status
714
+ }
715
+ }
716
+ }
717
+
718
+ override describe(): CollectorManifest {
719
+ return {
720
+ name: this.name,
721
+ records: false,
722
+ summary:
723
+ 'Lists every registered EntityManager with its storage, cache state, permissions, stats, fields and relations.',
724
+ entryShape: {
725
+ name: 'string',
726
+ system: 'boolean',
727
+ hasActivity: 'boolean',
728
+ label: 'string?',
729
+ labelPlural: 'string?',
730
+ routePrefix: 'string?',
731
+ idField: 'string?',
732
+ storage: 'StorageInfo',
733
+ multiStorage: 'MultiStorageInfo?',
734
+ cache: 'CacheInfo',
735
+ permissions: 'PermissionsInfo',
736
+ authSensitive: 'boolean',
737
+ warmup: 'WarmupInfo',
738
+ stats: 'StatsInfo',
739
+ fields: 'FieldsInfo',
740
+ relations: 'RelationsInfo',
741
+ },
742
+ stateShape: {
743
+ registered: 'string[] (entity names)',
744
+ activeEntities: 'string[] (entities with unseen activity)',
745
+ },
746
+ actions: [
747
+ ...this._builtinActionManifests(),
748
+ {
749
+ name: 'refreshCache',
750
+ summary: 'Invalidate one entity\'s cache (and optionally reload).',
751
+ args: { entity: 'string', reload: 'boolean?' },
752
+ mutates: true,
753
+ },
754
+ {
755
+ name: 'invalidateCache',
756
+ summary: 'Mark one entity\'s cache invalid without reloading.',
757
+ args: { entity: 'string' },
758
+ mutates: true,
759
+ },
760
+ {
761
+ name: 'testFetch',
762
+ summary: 'Probe an entity\'s primary storage with a list({page:1,page_size:1}).',
763
+ args: { entity: 'string' },
764
+ },
765
+ {
766
+ name: 'testStorageFetch',
767
+ summary: 'Probe a specific named storage on an entity.',
768
+ args: { entity: 'string', storage: 'string' },
769
+ },
770
+ ],
771
+ }
772
+ }
773
+
774
+ override snapshot(): CollectorSnapshot {
775
+ const entries = this.getEntries()
776
+ const registered = this._orchestrator?.getRegisteredNames() ?? []
777
+ return {
778
+ name: this.name,
779
+ entries,
780
+ count: entries.length,
781
+ unseen: this.getBadge(),
782
+ state: {
783
+ registered,
784
+ activeEntities: Array.from(this._activeEntities),
785
+ },
786
+ }
787
+ }
788
+
789
+ override async call(actionName: string, args: Record<string, unknown> = {}): Promise<unknown> {
790
+ if (actionName === 'refreshCache') {
791
+ const ok = await this.refreshCache(String(args.entity ?? ''), Boolean(args.reload))
792
+ return { ok }
793
+ }
794
+ if (actionName === 'invalidateCache') {
795
+ this.invalidateCache(String(args.entity ?? ''))
796
+ return { ok: true }
797
+ }
798
+ if (actionName === 'testFetch') {
799
+ return await this.testFetch(String(args.entity ?? ''))
800
+ }
801
+ if (actionName === 'testStorageFetch') {
802
+ return await this.testStorageFetch(String(args.entity ?? ''), String(args.storage ?? ''))
803
+ }
804
+ return super.call(actionName, args)
805
+ }
806
+
807
+ /**
808
+ * Test fetch data from a specific storage of an entity
809
+ */
810
+ async testStorageFetch(
811
+ entityName: string,
812
+ storageName: string
813
+ ): Promise<{ success: boolean; count?: number; error?: string; status?: number }> {
814
+ if (!this._orchestrator) {
815
+ return { success: false, error: 'No orchestrator' }
816
+ }
817
+ try {
818
+ const manager = this._orchestrator.get(entityName) as EntityManager & Record<string, unknown>
819
+ const storage = storageName === 'storage'
820
+ ? manager.storage as { list: (params: { page: number; page_size: number }) => Promise<{ total?: number; items?: unknown[] }> }
821
+ : manager[storageName] as { list: (params: { page: number; page_size: number }) => Promise<{ total?: number; items?: unknown[] }> } | undefined
822
+
823
+ if (!storage) {
824
+ return { success: false, error: `Storage '${storageName}' not found` }
825
+ }
826
+ const result = await storage.list({ page: 1, page_size: 1 })
827
+ return {
828
+ success: true,
829
+ count: result.total ?? result.items?.length ?? 0
830
+ }
831
+ } catch (e) {
832
+ return {
833
+ success: false,
834
+ error: (e as Error).message,
835
+ status: (e as Error & { status?: number }).status
836
+ }
837
+ }
838
+ }
839
+ }