@camstack/server 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -7
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +346 -202
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +54 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
- package/src/api/trpc/cap-mount-helpers.ts +64 -55
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +593 -351
- package/src/api/trpc/generated-cap-routers.ts +3680 -579
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +79 -46
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1163 -1305
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +12 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +120 -32
- package/src/core/network/network-quality.service.spec.ts +6 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +602 -531
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
|
@@ -18,21 +18,21 @@ export function createCapabilitiesRouter(
|
|
|
18
18
|
.input(z.void())
|
|
19
19
|
.query(() => registry?.listCapabilities() ?? []),
|
|
20
20
|
|
|
21
|
-
getCapability: adminProcedure
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
) ?? null
|
|
28
|
-
}),
|
|
21
|
+
getCapability: adminProcedure.input(z.object({ name: z.string() })).query(({ input }) => {
|
|
22
|
+
if (!registry) return null
|
|
23
|
+
return (
|
|
24
|
+
registry.listCapabilities().find((c: { name: string }) => c.name === input.name) ?? null
|
|
25
|
+
)
|
|
26
|
+
}),
|
|
29
27
|
|
|
30
28
|
setActiveSingleton: adminProcedure
|
|
31
|
-
.input(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
.input(
|
|
30
|
+
z.object({
|
|
31
|
+
capability: z.string(),
|
|
32
|
+
addonId: z.string(),
|
|
33
|
+
nodeId: z.string().optional(),
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
36
|
.output(z.void())
|
|
37
37
|
.mutation(async ({ input }) => {
|
|
38
38
|
if (!registry) throw new Error('Capability registry unavailable')
|
|
@@ -40,7 +40,10 @@ export function createCapabilitiesRouter(
|
|
|
40
40
|
if (input.nodeId !== undefined) {
|
|
41
41
|
// Per-node override: persist under the node-qualified key so the boot
|
|
42
42
|
// restorer (`setNodeConfigReader`) replays it on restart.
|
|
43
|
-
config.set(
|
|
43
|
+
config.set(
|
|
44
|
+
`capabilities.singletonNode.${input.capability}.${input.nodeId}`,
|
|
45
|
+
input.addonId,
|
|
46
|
+
)
|
|
44
47
|
} else {
|
|
45
48
|
// Cluster-global default: persist to the SAME key the boot restorer reads
|
|
46
49
|
// (addon-registry `setConfigReader` → `capabilities.singleton.<cap>`).
|
|
@@ -67,10 +70,12 @@ export function createCapabilitiesRouter(
|
|
|
67
70
|
* save endpoint for a bulk multi-select UI.
|
|
68
71
|
*/
|
|
69
72
|
setCollectionEnabledProviders: adminProcedure
|
|
70
|
-
.input(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
.input(
|
|
74
|
+
z.object({
|
|
75
|
+
capability: z.string(),
|
|
76
|
+
enabledAddonIds: z.array(z.string()),
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
74
79
|
.output(z.void())
|
|
75
80
|
.mutation(({ input }) => {
|
|
76
81
|
if (!registry) throw new Error('Capability registry unavailable')
|
|
@@ -120,11 +125,13 @@ export function createCapabilitiesRouter(
|
|
|
120
125
|
}),
|
|
121
126
|
|
|
122
127
|
setDeviceCollectionFilter: adminProcedure
|
|
123
|
-
.input(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
.input(
|
|
129
|
+
z.object({
|
|
130
|
+
deviceId: z.string(),
|
|
131
|
+
capability: z.string(),
|
|
132
|
+
addonIds: z.array(z.string()),
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
128
135
|
.output(z.void())
|
|
129
136
|
.mutation(({ input }) => {
|
|
130
137
|
if (!registry) throw new Error('Capability registry unavailable')
|
|
@@ -18,11 +18,22 @@ import type { HwAccelResolution } from '@camstack/types'
|
|
|
18
18
|
import { trpcRouter, adminProcedure } from '../trpc/trpc.middleware.js'
|
|
19
19
|
|
|
20
20
|
const HwAccelBackendSchema = z.enum([
|
|
21
|
-
'videotoolbox',
|
|
22
|
-
'
|
|
21
|
+
'videotoolbox',
|
|
22
|
+
'cuda',
|
|
23
|
+
'nvdec',
|
|
24
|
+
'vaapi',
|
|
25
|
+
'qsv',
|
|
26
|
+
'd3d11va',
|
|
27
|
+
'dxva2',
|
|
28
|
+
'amf',
|
|
29
|
+
'vdpau',
|
|
30
|
+
'drm',
|
|
23
31
|
])
|
|
24
32
|
|
|
25
|
-
const HwAccelPreferSchema = z
|
|
33
|
+
const HwAccelPreferSchema = z
|
|
34
|
+
.union([HwAccelBackendSchema, z.literal('none')])
|
|
35
|
+
.nullable()
|
|
36
|
+
.optional()
|
|
26
37
|
|
|
27
38
|
const HwAccelResolutionSchema = z.object({
|
|
28
39
|
preferred: z.array(HwAccelBackendSchema).readonly(),
|
|
@@ -48,23 +59,39 @@ export function createHwAccelRouter(broker: ServiceBroker | null) {
|
|
|
48
59
|
.query(async ({ input }) => {
|
|
49
60
|
if (!broker) throw new Error('Moleculer broker not available')
|
|
50
61
|
const params = { prefer: input.prefer ?? null }
|
|
51
|
-
return broker.call('$hwaccel.resolve', params, {
|
|
62
|
+
return broker.call('$hwaccel.resolve', params, {
|
|
63
|
+
nodeID: input.nodeId,
|
|
64
|
+
}) as Promise<HwAccelResolution>
|
|
52
65
|
}),
|
|
53
66
|
|
|
54
67
|
/** List every node currently reachable and the hwaccel each resolves to. */
|
|
55
68
|
resolveAll: adminProcedure
|
|
56
|
-
.output(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
.output(
|
|
70
|
+
z.array(
|
|
71
|
+
z.object({
|
|
72
|
+
nodeId: z.string(),
|
|
73
|
+
resolution: HwAccelResolutionSchema,
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
60
77
|
.query(async () => {
|
|
61
78
|
if (!broker) throw new Error('Moleculer broker not available')
|
|
62
|
-
const registry = (
|
|
79
|
+
const registry = (
|
|
80
|
+
broker as unknown as {
|
|
81
|
+
registry: {
|
|
82
|
+
getNodeList: (opts: { onlyAvailable: boolean }) => readonly { id: string }[]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
).registry
|
|
63
86
|
const nodes = registry.getNodeList({ onlyAvailable: true })
|
|
64
87
|
const results = await Promise.all(
|
|
65
88
|
nodes.map(async (n) => {
|
|
66
89
|
try {
|
|
67
|
-
const resolution = await broker.call(
|
|
90
|
+
const resolution = (await broker.call(
|
|
91
|
+
'$hwaccel.resolve',
|
|
92
|
+
{ prefer: null },
|
|
93
|
+
{ nodeID: n.id },
|
|
94
|
+
)) as HwAccelResolution
|
|
68
95
|
return { nodeId: n.id, resolution }
|
|
69
96
|
} catch (err) {
|
|
70
97
|
const msg = err instanceof Error ? err.message : String(err)
|
|
@@ -9,22 +9,29 @@ import type { EventBusService } from '../../core/events/event-bus.service.js'
|
|
|
9
9
|
import type { AddonRegistryService } from '../../core/addon/addon-registry.service.js'
|
|
10
10
|
import { trpcRouter, protectedProcedure, iterableSubscription } from '../trpc/trpc.middleware.js'
|
|
11
11
|
|
|
12
|
-
export function createLiveEventsRouter(
|
|
13
|
-
eb: EventBusService,
|
|
14
|
-
ar: AddonRegistryService,
|
|
15
|
-
) {
|
|
12
|
+
export function createLiveEventsRouter(eb: EventBusService, ar: AddonRegistryService) {
|
|
16
13
|
return trpcRouter({
|
|
17
14
|
recentSystemEvents: protectedProcedure
|
|
18
|
-
.input(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
.input(
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
category: z.string().optional(),
|
|
19
|
+
limit: z.number().optional(),
|
|
20
|
+
})
|
|
21
|
+
.optional(),
|
|
22
|
+
)
|
|
22
23
|
.query(({ input }) => eb.getRecent(input ?? {}, input?.limit ?? 50)),
|
|
23
24
|
|
|
24
25
|
onEvent: protectedProcedure
|
|
25
26
|
.input(z.object({ category: z.string().optional() }))
|
|
26
27
|
.subscription(({ input }) => {
|
|
27
|
-
return iterableSubscription<{
|
|
28
|
+
return iterableSubscription<{
|
|
29
|
+
id: string
|
|
30
|
+
timestamp: Date
|
|
31
|
+
source: { type: string; id: string | number }
|
|
32
|
+
category: string
|
|
33
|
+
data: Record<string, unknown>
|
|
34
|
+
}>((push) => {
|
|
28
35
|
const filter: Record<string, unknown> = {}
|
|
29
36
|
if (input.category) filter.category = input.category
|
|
30
37
|
return eb.subscribe(filter, push)
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod'
|
|
10
10
|
import type { LoggingService } from '../../core/logging/logging.service.js'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
trpcRouter,
|
|
13
|
+
protectedProcedure,
|
|
14
|
+
adminProcedure,
|
|
15
|
+
iterableSubscription,
|
|
16
|
+
} from '../trpc/trpc.middleware.js'
|
|
12
17
|
import type { LogEntry } from '@camstack/types'
|
|
13
18
|
|
|
14
19
|
const LogLevelSchema = z.enum(['debug', 'info', 'warn', 'error'])
|
|
@@ -59,13 +64,15 @@ const LogEntryArraySchema = z.array(LogEntrySchema)
|
|
|
59
64
|
export function createLogsRouter(logging: LoggingService) {
|
|
60
65
|
return trpcRouter({
|
|
61
66
|
query: adminProcedure
|
|
62
|
-
.input(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
.input(
|
|
68
|
+
z.object({
|
|
69
|
+
level: LogLevelSchema.optional(),
|
|
70
|
+
since: z.number().optional(),
|
|
71
|
+
until: z.number().optional(),
|
|
72
|
+
limit: z.number().optional(),
|
|
73
|
+
tags: LogTagsSchema.optional(),
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
69
76
|
.output(LogEntryArraySchema)
|
|
70
77
|
.query(({ input, ctx }) => {
|
|
71
78
|
const filter: Record<string, unknown> = { limit: input.limit ?? 100 }
|
|
@@ -79,10 +86,10 @@ export function createLogsRouter(logging: LoggingService) {
|
|
|
79
86
|
const ap = ctx.user.permissions?.allowedProviders
|
|
80
87
|
if (ap && ap !== '*') {
|
|
81
88
|
const allowed = ap as string[]
|
|
82
|
-
return entries.filter(e => {
|
|
89
|
+
return entries.filter((e) => {
|
|
83
90
|
const addonId = e.tags?.addonId
|
|
84
91
|
if (typeof addonId !== 'string') return false
|
|
85
|
-
return allowed.some(p => {
|
|
92
|
+
return allowed.some((p) => {
|
|
86
93
|
const bare = p.startsWith('addon:') ? p.slice('addon:'.length) : p
|
|
87
94
|
return addonId === bare || addonId.startsWith(bare)
|
|
88
95
|
})
|
|
@@ -91,7 +98,7 @@ export function createLogsRouter(logging: LoggingService) {
|
|
|
91
98
|
}
|
|
92
99
|
return entries
|
|
93
100
|
})()
|
|
94
|
-
return filtered.map(e => ({
|
|
101
|
+
return filtered.map((e) => ({
|
|
95
102
|
timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : String(e.timestamp),
|
|
96
103
|
level: e.level,
|
|
97
104
|
...(e.scope !== undefined ? { scope: e.scope } : {}),
|
|
@@ -102,7 +109,10 @@ export function createLogsRouter(logging: LoggingService) {
|
|
|
102
109
|
// the same normalisation applied in `subscribe` below.
|
|
103
110
|
tags: e.tags
|
|
104
111
|
? Object.fromEntries(
|
|
105
|
-
Object.entries(e.tags).map(([k, v]) => [
|
|
112
|
+
Object.entries(e.tags).map(([k, v]) => [
|
|
113
|
+
k,
|
|
114
|
+
v === undefined ? undefined : String(v),
|
|
115
|
+
]),
|
|
106
116
|
)
|
|
107
117
|
: undefined,
|
|
108
118
|
...(e.meta !== undefined ? { meta: e.meta } : {}),
|
|
@@ -117,12 +127,14 @@ export function createLogsRouter(logging: LoggingService) {
|
|
|
117
127
|
* are reading.
|
|
118
128
|
*/
|
|
119
129
|
clear: adminProcedure
|
|
120
|
-
.input(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
.input(
|
|
131
|
+
z.object({
|
|
132
|
+
level: LogLevelSchema.optional(),
|
|
133
|
+
since: z.number().optional(),
|
|
134
|
+
until: z.number().optional(),
|
|
135
|
+
tags: LogTagsSchema.optional(),
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
126
138
|
.output(z.object({ removed: z.number().int().nonnegative() }))
|
|
127
139
|
.mutation(({ input }) => {
|
|
128
140
|
const filter: Record<string, unknown> = {}
|
|
@@ -135,10 +147,12 @@ export function createLogsRouter(logging: LoggingService) {
|
|
|
135
147
|
}),
|
|
136
148
|
|
|
137
149
|
subscribe: protectedProcedure
|
|
138
|
-
.input(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
.input(
|
|
151
|
+
z.object({
|
|
152
|
+
level: LogLevelSchema.optional(),
|
|
153
|
+
tags: LogTagsSchema.optional(),
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
142
156
|
.subscription(({ input }) => {
|
|
143
157
|
return iterableSubscription<unknown>((push) => {
|
|
144
158
|
return logging.subscribe(
|
|
@@ -146,12 +160,27 @@ export function createLogsRouter(logging: LoggingService) {
|
|
|
146
160
|
level: input.level,
|
|
147
161
|
tags: input.tags as Record<string, string> | undefined,
|
|
148
162
|
},
|
|
149
|
-
(entry: {
|
|
163
|
+
(entry: {
|
|
164
|
+
timestamp: Date | string | number
|
|
165
|
+
level: string
|
|
166
|
+
message: string
|
|
167
|
+
scope?: string
|
|
168
|
+
tags?: Record<string, string | number | undefined>
|
|
169
|
+
meta?: Record<string, unknown>
|
|
170
|
+
}) => {
|
|
150
171
|
const stringifiedTags = entry.tags
|
|
151
|
-
? Object.fromEntries(
|
|
172
|
+
? Object.fromEntries(
|
|
173
|
+
Object.entries(entry.tags).map(([k, v]) => [
|
|
174
|
+
k,
|
|
175
|
+
v === undefined ? undefined : String(v),
|
|
176
|
+
]),
|
|
177
|
+
)
|
|
152
178
|
: undefined
|
|
153
179
|
push({
|
|
154
|
-
timestamp:
|
|
180
|
+
timestamp:
|
|
181
|
+
entry.timestamp instanceof Date
|
|
182
|
+
? entry.timestamp.toISOString()
|
|
183
|
+
: String(entry.timestamp),
|
|
155
184
|
level: entry.level,
|
|
156
185
|
message: entry.message,
|
|
157
186
|
scope: entry.scope,
|
|
@@ -58,7 +58,8 @@ export function createNotificationsRouter(ns: NotificationService | null) {
|
|
|
58
58
|
if (!ns) return { success: false, error: 'Notification service unavailable' }
|
|
59
59
|
const output = ns.getOutput(input.outputId)
|
|
60
60
|
if (!output) return { success: false, error: `Output "${input.outputId}" not found` }
|
|
61
|
-
if (!output.sendTest)
|
|
61
|
+
if (!output.sendTest)
|
|
62
|
+
return { success: false, error: 'Output does not support test notifications' }
|
|
62
63
|
return output.sendTest()
|
|
63
64
|
}),
|
|
64
65
|
})
|
|
@@ -27,9 +27,7 @@ export function createReplRouter(repl: ReplEngineService) {
|
|
|
27
27
|
execute: adminProcedure
|
|
28
28
|
.input(z.object({ code: z.string().max(10000), scope: ReplScopeSchema }))
|
|
29
29
|
.output(ReplResultSchema)
|
|
30
|
-
.mutation(({ input }) =>
|
|
31
|
-
repl.execute(input.code, { scope: input.scope, variables: {} }),
|
|
32
|
-
),
|
|
30
|
+
.mutation(({ input }) => repl.execute(input.code, { scope: input.scope, variables: {} })),
|
|
33
31
|
|
|
34
32
|
completions: adminProcedure
|
|
35
33
|
.input(z.object({ partial: z.string(), scope: ReplScopeSchema }))
|
|
@@ -27,17 +27,21 @@ const SetValueSchema = z.object({
|
|
|
27
27
|
value: z.unknown(),
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
const QueryFilterSchema = z
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const QueryFilterSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
where: z.record(z.string(), z.unknown()).optional(),
|
|
33
|
+
whereIn: z.record(z.string(), z.array(z.unknown())).optional(),
|
|
34
|
+
whereBetween: z.record(z.string(), z.tuple([z.unknown(), z.unknown()])).optional(),
|
|
35
|
+
orderBy: z
|
|
36
|
+
.object({
|
|
37
|
+
field: z.string(),
|
|
38
|
+
direction: z.enum(['asc', 'desc']),
|
|
39
|
+
})
|
|
40
|
+
.optional(),
|
|
41
|
+
limit: z.number().optional(),
|
|
42
|
+
offset: z.number().optional(),
|
|
43
|
+
})
|
|
44
|
+
.optional()
|
|
41
45
|
|
|
42
46
|
const QueryInputSchema = z.object({
|
|
43
47
|
collection: z.string(),
|
|
@@ -71,72 +75,66 @@ const IsEmptyInputSchema = z.object({
|
|
|
71
75
|
// Router factory
|
|
72
76
|
// ---------------------------------------------------------------------------
|
|
73
77
|
|
|
74
|
-
export function createSettingsBackendRouter(
|
|
75
|
-
getBackend: () => ISettingsBackend | null,
|
|
76
|
-
) {
|
|
78
|
+
export function createSettingsBackendRouter(getBackend: () => ISettingsBackend | null) {
|
|
77
79
|
const requireBackend = (): ISettingsBackend => {
|
|
78
80
|
const backend = getBackend()
|
|
79
81
|
if (!backend) {
|
|
80
|
-
throw new Error(
|
|
82
|
+
throw new Error(
|
|
83
|
+
'Settings backend not available — settings-store addon may not be initialized yet',
|
|
84
|
+
)
|
|
81
85
|
}
|
|
82
86
|
return backend
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
return trpcRouter({
|
|
86
|
-
get: protectedProcedure
|
|
87
|
-
.input
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
query: protectedProcedure
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
isEmpty: protectedProcedure
|
|
136
|
-
.input(IsEmptyInputSchema)
|
|
137
|
-
.query(async ({ input }) => {
|
|
138
|
-
const result = await requireBackend().isEmpty(input)
|
|
139
|
-
return { empty: result }
|
|
140
|
-
}),
|
|
90
|
+
get: protectedProcedure.input(CollectionKeySchema).query(async ({ input }) => {
|
|
91
|
+
const result = await requireBackend().get(input)
|
|
92
|
+
return { value: result }
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
set: protectedProcedure.input(SetValueSchema).mutation(async ({ input }) => {
|
|
96
|
+
await requireBackend().set({
|
|
97
|
+
collection: input.collection,
|
|
98
|
+
key: input.key,
|
|
99
|
+
value: input.value,
|
|
100
|
+
})
|
|
101
|
+
return { success: true as const }
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
query: protectedProcedure.input(QueryInputSchema).query(async ({ input }) => {
|
|
105
|
+
const records = await requireBackend().query({
|
|
106
|
+
collection: input.collection,
|
|
107
|
+
filter: input.filter ?? undefined,
|
|
108
|
+
})
|
|
109
|
+
return { records: records.map((r) => ({ id: r.id, data: r.data })) }
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
insert: protectedProcedure.input(InsertInputSchema).mutation(async ({ input }) => {
|
|
113
|
+
await requireBackend().insert(input)
|
|
114
|
+
return { success: true as const }
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
update: protectedProcedure.input(UpdateInputSchema).mutation(async ({ input }) => {
|
|
118
|
+
await requireBackend().update(input)
|
|
119
|
+
return { success: true as const }
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
delete: protectedProcedure.input(CollectionKeySchema).mutation(async ({ input }) => {
|
|
123
|
+
await requireBackend().delete(input)
|
|
124
|
+
return { success: true as const }
|
|
125
|
+
}),
|
|
126
|
+
|
|
127
|
+
count: protectedProcedure.input(CountInputSchema).query(async ({ input }) => {
|
|
128
|
+
const result = await requireBackend().count({
|
|
129
|
+
collection: input.collection,
|
|
130
|
+
filter: input.filter ?? undefined,
|
|
131
|
+
})
|
|
132
|
+
return { count: result }
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
isEmpty: protectedProcedure.input(IsEmptyInputSchema).query(async ({ input }) => {
|
|
136
|
+
const result = await requireBackend().isEmpty(input)
|
|
137
|
+
return { empty: result }
|
|
138
|
+
}),
|
|
141
139
|
})
|
|
142
140
|
}
|
|
@@ -14,7 +14,13 @@ import { trpcRouter, protectedProcedure, iterableSubscription } from '../trpc/tr
|
|
|
14
14
|
type SerializedEvent = {
|
|
15
15
|
id: string
|
|
16
16
|
timestamp: string
|
|
17
|
-
source: {
|
|
17
|
+
source: {
|
|
18
|
+
type: string
|
|
19
|
+
id: string | number
|
|
20
|
+
nodeId?: string
|
|
21
|
+
addonId?: string
|
|
22
|
+
deviceId?: number
|
|
23
|
+
}
|
|
18
24
|
category: string
|
|
19
25
|
data: unknown
|
|
20
26
|
}
|
|
@@ -42,10 +48,12 @@ function serialize(e: SystemEvent): SerializedEvent {
|
|
|
42
48
|
*/
|
|
43
49
|
const ScopeFieldsSchema = z.object({
|
|
44
50
|
/** Legacy source filter (exact type+id match). */
|
|
45
|
-
source: z
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
source: z
|
|
52
|
+
.object({
|
|
53
|
+
type: z.string(),
|
|
54
|
+
id: z.union([z.string(), z.number()]),
|
|
55
|
+
})
|
|
56
|
+
.optional(),
|
|
49
57
|
/** Agent/node filter (prefix match: 'hub' matches 'hub/pipeline'). */
|
|
50
58
|
agentId: z.string().optional(),
|
|
51
59
|
/** Addon filter. Matches source.addonId or source.id when type='addon'. */
|
|
@@ -84,33 +92,34 @@ const SubscribeInputSchema = ScopeFieldsSchema.extend({
|
|
|
84
92
|
|
|
85
93
|
export function createSystemEventsRouter(eb: EventBusService) {
|
|
86
94
|
return trpcRouter({
|
|
87
|
-
getRecent: protectedProcedure
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
getRecent: protectedProcedure.input(GetRecentInputSchema).query(({ input }) => {
|
|
96
|
+
return eb
|
|
97
|
+
.getRecent(
|
|
98
|
+
{
|
|
99
|
+
...(input.source ? { source: input.source } : {}),
|
|
100
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
101
|
+
...(input.addonId ? { addonId: input.addonId } : {}),
|
|
102
|
+
...(input.deviceId !== undefined ? { deviceId: input.deviceId } : {}),
|
|
103
|
+
...(input.category ? { category: input.category } : {}),
|
|
104
|
+
},
|
|
105
|
+
input.limit,
|
|
106
|
+
)
|
|
107
|
+
.map(serialize)
|
|
108
|
+
}),
|
|
98
109
|
|
|
99
|
-
subscribe: protectedProcedure
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})
|
|
114
|
-
}),
|
|
110
|
+
subscribe: protectedProcedure.input(SubscribeInputSchema).subscription(({ input }) => {
|
|
111
|
+
return iterableSubscription<unknown>((push) => {
|
|
112
|
+
return eb.subscribe(
|
|
113
|
+
{
|
|
114
|
+
...(input.source ? { source: input.source } : {}),
|
|
115
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
116
|
+
...(input.addonId ? { addonId: input.addonId } : {}),
|
|
117
|
+
...(input.deviceId !== undefined ? { deviceId: input.deviceId } : {}),
|
|
118
|
+
...(input.category ? { category: input.category } : {}),
|
|
119
|
+
},
|
|
120
|
+
(event: SystemEvent) => push(serialize(event)),
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
}),
|
|
115
124
|
})
|
|
116
125
|
}
|
|
@@ -14,12 +14,7 @@
|
|
|
14
14
|
* and monitors that talk directly to an agent see identical payloads.
|
|
15
15
|
*/
|
|
16
16
|
import type { FastifyInstance, FastifyReply } from 'fastify'
|
|
17
|
-
import type {
|
|
18
|
-
AgentHealth,
|
|
19
|
-
AgentHealthError,
|
|
20
|
-
ClusterHealth,
|
|
21
|
-
HubHealth,
|
|
22
|
-
} from '@camstack/types'
|
|
17
|
+
import type { AgentHealth, AgentHealthError, ClusterHealth, HubHealth } from '@camstack/types'
|
|
23
18
|
import type { MoleculerService } from '../../core/moleculer/moleculer.service'
|
|
24
19
|
import type { AgentRegistryService } from '../../core/agent/agent-registry.service'
|
|
25
20
|
|
|
@@ -42,7 +37,10 @@ function nowIso(): string {
|
|
|
42
37
|
return new Date().toISOString()
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
async function buildHubHealth(
|
|
40
|
+
async function buildHubHealth(
|
|
41
|
+
deps: HealthRoutesDeps,
|
|
42
|
+
proc: ProcessLike = process,
|
|
43
|
+
): Promise<HubHealth> {
|
|
46
44
|
const nodes = await deps.agentRegistry.listNodes()
|
|
47
45
|
const remote = nodes.filter((n) => !n.isHub)
|
|
48
46
|
const online = remote.filter((n) => n.isOnline !== false).length
|
|
@@ -89,9 +87,7 @@ export function registerHealthRoutes(fastify: FastifyInstance, deps: HealthRoute
|
|
|
89
87
|
fastify.get('/health/agents', async (): Promise<{ readonly agents: readonly string[] }> => {
|
|
90
88
|
const nodes = await deps.agentRegistry.listNodes()
|
|
91
89
|
return {
|
|
92
|
-
agents: nodes
|
|
93
|
-
.filter((n) => !n.isHub && n.isOnline !== false)
|
|
94
|
-
.map((n) => n.info.id),
|
|
90
|
+
agents: nodes.filter((n) => !n.isHub && n.isOnline !== false).map((n) => n.info.id),
|
|
95
91
|
}
|
|
96
92
|
})
|
|
97
93
|
|
|
@@ -114,9 +110,7 @@ export function registerHealthRoutes(fastify: FastifyInstance, deps: HealthRoute
|
|
|
114
110
|
const hub = await buildHubHealth(deps)
|
|
115
111
|
const nodes = await deps.agentRegistry.listNodes()
|
|
116
112
|
const remote = nodes.filter((n) => !n.isHub && n.isOnline !== false)
|
|
117
|
-
const agents = await Promise.all(
|
|
118
|
-
remote.map((n) => fetchAgentHealth(deps, n.info.id)),
|
|
119
|
-
)
|
|
113
|
+
const agents = await Promise.all(remote.map((n) => fetchAgentHealth(deps, n.info.id)))
|
|
120
114
|
const ok = hub.ok && agents.every((a) => a.ok)
|
|
121
115
|
return { ok, hub, agents, checkedAt: nowIso() }
|
|
122
116
|
})
|