@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,401 @@
1
+ /**
2
+ * qdadm Vite dev plugin — exposes the running app's DebugBridge over HTTP.
3
+ *
4
+ * Endpoints (default prefix `/__qdadm`):
5
+ * GET /__qdadm/ — index of endpoints + cache stats
6
+ * GET /__qdadm/sessions — list of connected client sessions
7
+ * GET /__qdadm/describe.json — bridge.describe() (manifests)
8
+ * GET /__qdadm/snapshot.json — bridge.dump() (live state)
9
+ * POST /__qdadm/call — body {collector, action, args?}
10
+ *
11
+ * Session selection: every browser tab that loads the app gets a fresh
12
+ * session id (uuid in the injected client). All endpoints accept a
13
+ * `?session=<id|latest>` query param. Default is `latest` (the most recently
14
+ * active session).
15
+ *
16
+ * Wire-up: the plugin injects a small ESM script via `transformIndexHtml`.
17
+ * That inline script lives in Vite's HMR pipeline (so `import.meta.hot`
18
+ * works), polls for `window.__qdadm.debug.bridge`, then pushes describe +
19
+ * snapshot on every reactive tick. The plugin caches per-session payloads
20
+ * in memory; GETs are served from cache (no round-trip needed).
21
+ *
22
+ * `call` is always request-response — it mutates state in the live page.
23
+ *
24
+ * Cache responses include `X-Qdadm-Stale-Ms`, `X-Qdadm-Source: cache|live`,
25
+ * and `X-Qdadm-Session: <id>` headers.
26
+ */
27
+
28
+ import type { Plugin, ViteDevServer } from 'vite'
29
+ import type { IncomingMessage, ServerResponse } from 'node:http'
30
+
31
+ interface PendingRequest {
32
+ resolve: (data: unknown) => void
33
+ reject: (err: Error) => void
34
+ timeout: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ interface DebugRequest {
38
+ id: string
39
+ type: 'describe' | 'dump' | 'call'
40
+ payload?: unknown
41
+ /** Target a specific session (server → client). */
42
+ sessionId?: string
43
+ }
44
+
45
+ interface DebugReply {
46
+ id: string
47
+ ok: boolean
48
+ data?: unknown
49
+ error?: string
50
+ sessionId?: string
51
+ }
52
+
53
+ interface DebugPush {
54
+ type: 'describe' | 'snapshot' | 'hello' | 'bye'
55
+ data?: unknown
56
+ sessionId: string
57
+ /** Free-form metadata sent on hello (user agent, route, etc.). */
58
+ meta?: Record<string, unknown>
59
+ }
60
+
61
+ interface SessionState {
62
+ id: string
63
+ firstSeenAt: number
64
+ lastSeenAt: number
65
+ describe: { data: unknown; at: number } | null
66
+ snapshot: { data: unknown; at: number } | null
67
+ meta: Record<string, unknown>
68
+ }
69
+
70
+ export interface QdadmDebugPluginOptions {
71
+ /** URL prefix (default '/__qdadm') */
72
+ prefix?: string
73
+ /** Request timeout ms for live fallback (default 3000) */
74
+ timeoutMs?: number
75
+ /** Drop sessions inactive for this many ms (default 10 min) */
76
+ sessionTtlMs?: number
77
+ }
78
+
79
+ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin {
80
+ const prefix = options.prefix ?? '/__qdadm'
81
+ const timeoutMs = options.timeoutMs ?? 3000
82
+ const sessionTtlMs = options.sessionTtlMs ?? 10 * 60 * 1000
83
+
84
+ const sessions = new Map<string, SessionState>()
85
+ const pending = new Map<string, PendingRequest>()
86
+ let server: ViteDevServer | null = null
87
+ let nextId = 1
88
+
89
+ function pruneStaleSessions(): void {
90
+ const cutoff = Date.now() - sessionTtlMs
91
+ for (const [id, s] of sessions) {
92
+ if (s.lastSeenAt < cutoff) sessions.delete(id)
93
+ }
94
+ }
95
+
96
+ function pickSession(sessionParam: string | null): SessionState | null {
97
+ pruneStaleSessions()
98
+ if (!sessionParam || sessionParam === 'latest') {
99
+ let best: SessionState | null = null
100
+ for (const s of sessions.values()) {
101
+ if (!best || s.lastSeenAt > best.lastSeenAt) best = s
102
+ }
103
+ return best
104
+ }
105
+ return sessions.get(sessionParam) ?? null
106
+ }
107
+
108
+ function ask(
109
+ type: DebugRequest['type'],
110
+ payload?: unknown,
111
+ sessionId?: string
112
+ ): Promise<unknown> {
113
+ if (!server) return Promise.reject(new Error('[qdadm-debug] vite server not ready'))
114
+ const id = String(nextId++)
115
+ return new Promise((resolve, reject) => {
116
+ const timeout = setTimeout(() => {
117
+ pending.delete(id)
118
+ reject(
119
+ new Error(
120
+ `[qdadm-debug] timeout waiting for ${type} — open the app in a browser at least once`
121
+ )
122
+ )
123
+ }, timeoutMs)
124
+ pending.set(id, { resolve, reject, timeout })
125
+ server!.ws.send('qdadm:debug:request', { id, type, payload, sessionId } satisfies DebugRequest)
126
+ })
127
+ }
128
+
129
+ async function serveCachedOrLive(
130
+ res: ServerResponse,
131
+ sessionParam: string | null,
132
+ field: 'describe' | 'snapshot',
133
+ liveType: 'describe' | 'dump'
134
+ ): Promise<void> {
135
+ const session = pickSession(sessionParam)
136
+ if (session) {
137
+ const cache = session[field]
138
+ if (cache) {
139
+ res.setHeader('x-qdadm-stale-ms', String(Date.now() - cache.at))
140
+ res.setHeader('x-qdadm-source', 'cache')
141
+ res.setHeader('x-qdadm-session', session.id)
142
+ return sendJson(res, 200, cache.data)
143
+ }
144
+ }
145
+ try {
146
+ const data = await ask(liveType, undefined, session?.id)
147
+ res.setHeader('x-qdadm-stale-ms', '0')
148
+ res.setHeader('x-qdadm-source', 'live')
149
+ if (session) res.setHeader('x-qdadm-session', session.id)
150
+ return sendJson(res, 200, data)
151
+ } catch (e) {
152
+ return sendJson(res, 503, {
153
+ error: (e as Error).message,
154
+ sessions: listSessions(),
155
+ hint:
156
+ sessions.size === 0
157
+ ? 'No connected client. Load http://<dev>/ in a browser once.'
158
+ : 'No cached payload yet for the chosen session — try ?session=latest.',
159
+ })
160
+ }
161
+ }
162
+
163
+ function listSessions(): Array<{
164
+ id: string
165
+ firstSeenAt: number
166
+ lastSeenAt: number
167
+ ageMs: number
168
+ hasDescribe: boolean
169
+ hasSnapshot: boolean
170
+ meta: Record<string, unknown>
171
+ }> {
172
+ pruneStaleSessions()
173
+ const now = Date.now()
174
+ return Array.from(sessions.values())
175
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt)
176
+ .map((s) => ({
177
+ id: s.id,
178
+ firstSeenAt: s.firstSeenAt,
179
+ lastSeenAt: s.lastSeenAt,
180
+ ageMs: now - s.lastSeenAt,
181
+ hasDescribe: !!s.describe,
182
+ hasSnapshot: !!s.snapshot,
183
+ meta: s.meta,
184
+ }))
185
+ }
186
+
187
+ function ensureSession(id: string, meta?: Record<string, unknown>): SessionState {
188
+ let s = sessions.get(id)
189
+ if (!s) {
190
+ s = {
191
+ id,
192
+ firstSeenAt: Date.now(),
193
+ lastSeenAt: Date.now(),
194
+ describe: null,
195
+ snapshot: null,
196
+ meta: meta ?? {},
197
+ }
198
+ sessions.set(id, s)
199
+ } else {
200
+ s.lastSeenAt = Date.now()
201
+ if (meta) s.meta = { ...s.meta, ...meta }
202
+ }
203
+ return s
204
+ }
205
+
206
+ return {
207
+ name: 'qdadm-debug',
208
+ apply: 'serve',
209
+
210
+ transformIndexHtml: {
211
+ order: 'pre',
212
+ handler() {
213
+ return [
214
+ {
215
+ tag: 'script',
216
+ attrs: { type: 'module' },
217
+ injectTo: 'head' as const,
218
+ children: HMR_CLIENT_SCRIPT,
219
+ },
220
+ ]
221
+ },
222
+ },
223
+
224
+ configureServer(s) {
225
+ server = s
226
+
227
+ s.ws.on('qdadm:debug:reply', (data: DebugReply) => {
228
+ const p = pending.get(data.id)
229
+ if (!p) return
230
+ clearTimeout(p.timeout)
231
+ pending.delete(data.id)
232
+ if (data.ok) p.resolve(data.data)
233
+ else p.reject(new Error(data.error ?? '[qdadm-debug] unknown error'))
234
+ })
235
+
236
+ s.ws.on('qdadm:debug:push', (msg: DebugPush) => {
237
+ if (!msg?.sessionId) return
238
+ const session = ensureSession(msg.sessionId, msg.meta)
239
+ const at = Date.now()
240
+ if (msg.type === 'describe' && msg.data !== undefined) {
241
+ session.describe = { data: msg.data, at }
242
+ } else if (msg.type === 'snapshot' && msg.data !== undefined) {
243
+ session.snapshot = { data: msg.data, at }
244
+ } else if (msg.type === 'bye') {
245
+ sessions.delete(msg.sessionId)
246
+ }
247
+ })
248
+
249
+ s.middlewares.use(async (req, res, next) => {
250
+ if (!req.url || !req.url.startsWith(prefix)) return next()
251
+ const url = new URL(req.url, 'http://localhost')
252
+ const path = url.pathname.slice(prefix.length)
253
+ const sessionParam = url.searchParams.get('session')
254
+
255
+ try {
256
+ if (req.method === 'GET' && (path === '' || path === '/')) {
257
+ return sendJson(res, 200, {
258
+ version: '1',
259
+ description:
260
+ 'qdadm debug bridge — read collector manifests and snapshots, invoke actions.',
261
+ endpoints: {
262
+ 'GET /sessions': 'list connected client sessions',
263
+ 'GET /describe.json?session=<id|latest>':
264
+ 'manifest for every collector (entry shapes + actions)',
265
+ 'GET /snapshot.json?session=<id|latest>':
266
+ 'live JSON snapshot of every collector',
267
+ 'POST /call?session=<id|latest>':
268
+ 'invoke an action — body { collector, action, args? }',
269
+ },
270
+ sessions: listSessions(),
271
+ prefix,
272
+ })
273
+ }
274
+ if (req.method === 'GET' && path === '/sessions') {
275
+ return sendJson(res, 200, { sessions: listSessions() })
276
+ }
277
+ if (req.method === 'GET' && path === '/describe.json') {
278
+ return await serveCachedOrLive(res, sessionParam, 'describe', 'describe')
279
+ }
280
+ if (req.method === 'GET' && path === '/snapshot.json') {
281
+ return await serveCachedOrLive(res, sessionParam, 'snapshot', 'dump')
282
+ }
283
+ if (req.method === 'POST' && path === '/call') {
284
+ const body = await readBody(req)
285
+ const parsed = body ? JSON.parse(body) : {}
286
+ const session = pickSession(sessionParam)
287
+ if (!session) {
288
+ return sendJson(res, 503, {
289
+ error: 'no connected session',
290
+ sessions: listSessions(),
291
+ })
292
+ }
293
+ const data = await ask('call', parsed, session.id)
294
+ res.setHeader('x-qdadm-session', session.id)
295
+ return sendJson(res, 200, data)
296
+ }
297
+ return sendJson(res, 404, { error: `unknown ${req.method} ${path}` })
298
+ } catch (e) {
299
+ return sendJson(res, 500, { error: (e as Error).message })
300
+ }
301
+ })
302
+ },
303
+ }
304
+ }
305
+
306
+ export default qdadmDebugPlugin
307
+
308
+ function sendJson(res: ServerResponse, status: number, data: unknown): void {
309
+ res.statusCode = status
310
+ res.setHeader('content-type', 'application/json; charset=utf-8')
311
+ res.setHeader('cache-control', 'no-store')
312
+ res.end(JSON.stringify(data, null, 2))
313
+ }
314
+
315
+ function readBody(req: IncomingMessage): Promise<string> {
316
+ return new Promise((resolve, reject) => {
317
+ let buf = ''
318
+ req.setEncoding('utf-8')
319
+ req.on('data', (chunk: string) => {
320
+ buf += chunk
321
+ })
322
+ req.on('end', () => resolve(buf))
323
+ req.on('error', reject)
324
+ })
325
+ }
326
+
327
+ /**
328
+ * Client-side bridge — injected verbatim into index.html as an ES module.
329
+ * Lives in Vite's HMR pipeline (so `import.meta.hot` works here).
330
+ *
331
+ * Each tab gets its own session id (random uuid). Pushes describe + snapshot
332
+ * on every reactive tick of the bridge so the plugin's per-session cache
333
+ * stays fresh. Listens for ad-hoc requests targeted at this session.
334
+ */
335
+ const HMR_CLIENT_SCRIPT = String.raw`
336
+ const hot = import.meta.hot
337
+ if (!hot) {
338
+ console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
339
+ } else {
340
+ const sessionId = crypto.randomUUID()
341
+ const meta = { userAgent: navigator.userAgent, location: location.pathname }
342
+ let bridge = null
343
+ let lastTick = -1
344
+
345
+ const w = /** @type {any} */ (window)
346
+
347
+ function helloIfNeeded() {
348
+ try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
349
+ }
350
+ function pushDescribe() {
351
+ try { hot.send('qdadm:debug:push', { type: 'describe', sessionId, data: bridge.describe() }) }
352
+ catch (e) { console.warn('[qdadm-debug] describe push failed:', e) }
353
+ }
354
+ function pushSnapshot() {
355
+ try { hot.send('qdadm:debug:push', { type: 'snapshot', sessionId, data: bridge.dump() }) }
356
+ catch (e) { console.warn('[qdadm-debug] snapshot push failed:', e) }
357
+ }
358
+ function bye() {
359
+ try { hot.send('qdadm:debug:push', { type: 'bye', sessionId }) } catch (e) {}
360
+ }
361
+ window.addEventListener('beforeunload', bye)
362
+
363
+ hot.on('qdadm:debug:request', async (msg) => {
364
+ // Ignore requests addressed to a different session.
365
+ if (msg && msg.sessionId && msg.sessionId !== sessionId) return
366
+ const id = (msg && msg.id) || ''
367
+ try {
368
+ const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
369
+ if (!b) throw new Error('debug bridge not ready')
370
+ let data
371
+ if (msg.type === 'describe') data = b.describe()
372
+ else if (msg.type === 'dump') data = b.dump()
373
+ else if (msg.type === 'call') {
374
+ const p = msg.payload || {}
375
+ if (!p.collector || !p.action) throw new Error('call payload requires { collector, action, args? }')
376
+ data = await b.call(p.collector, p.action, p.args || {})
377
+ } else throw new Error('unknown request type "' + msg.type + '"')
378
+ hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId })
379
+ } catch (e) {
380
+ hot.send('qdadm:debug:reply', { id, ok: false, error: e && e.message ? e.message : String(e), sessionId })
381
+ }
382
+ })
383
+
384
+ let attachTimer = setInterval(() => {
385
+ bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
386
+ if (!bridge) return
387
+ clearInterval(attachTimer)
388
+ helloIfNeeded()
389
+ pushDescribe()
390
+ pushSnapshot()
391
+ lastTick = bridge.tick && bridge.tick.value
392
+ console.log('[qdadm-debug] HTTP bridge wired — session', sessionId.slice(0, 8))
393
+ setInterval(() => {
394
+ if (!bridge || !bridge.tick) return
395
+ if (bridge.tick.value === lastTick) return
396
+ lastTick = bridge.tick.value
397
+ pushSnapshot()
398
+ }, 250)
399
+ }, 100)
400
+ }
401
+ `
@@ -0,0 +1,50 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.svg' {
4
+ const content: string
5
+ export default content
6
+ }
7
+
8
+ declare module '*.vue' {
9
+ import type { DefineComponent } from 'vue'
10
+ const component: DefineComponent<object, object, unknown>
11
+ export default component
12
+ }
13
+
14
+ // Style modules. The `vite/client` triple-slash above already declares these,
15
+ // but consumers running `vue-tsc --noEmit` on qdadm's source may not always
16
+ // resolve `vite/client` (qdadm doesn't ship vite as a hard dep, and consumer
17
+ // tsconfigs sometimes scope `compilerOptions.types` narrowly). Declaring them
18
+ // here directly makes qdadm's type surface self-sufficient — the scss imports
19
+ // in DebugModule, NotificationModule, and Module.styles() type-check from any
20
+ // downstream context.
21
+ declare module '*.scss' {
22
+ const content: string
23
+ export default content
24
+ }
25
+
26
+ declare module '*.css' {
27
+ const content: string
28
+ export default content
29
+ }
30
+
31
+ declare module '*.sass' {
32
+ const content: string
33
+ export default content
34
+ }
35
+
36
+ // `pluralize` ships as plain JS without bundled `.d.ts`. The official
37
+ // types live in `@types/pluralize` but pulling them in as a runtime
38
+ // dep here is overkill for a single-call usage. Declare the minimum
39
+ // surface we use so `strict` mode is happy without DefinitelyTyped.
40
+ declare module 'pluralize' {
41
+ interface PluralizeFn {
42
+ (word: string, count?: number, inclusive?: boolean): string
43
+ singular(word: string): string
44
+ plural(word: string, count?: number, inclusive?: boolean): string
45
+ isPlural(word: string): boolean
46
+ isSingular(word: string): boolean
47
+ }
48
+ const pluralize: PluralizeFn
49
+ export default pluralize
50
+ }