@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- 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
|
+
}
|