@astrale-os/adapter-cloudflare 0.1.9 → 0.1.10
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 +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +1 -1
- package/template/.agents/skills/astrale-domain/SKILL.md +5 -5
- package/template/.env.example +5 -7
- package/template/README.md +2 -2
- package/template/client/README.md +79 -62
- package/template/client/__tests__/app.test.tsx +188 -99
- package/template/client/__tests__/harness.ts +67 -12
- package/template/client/__tests__/kernel.test.ts +65 -50
- package/template/client/__tests__/seam.test.tsx +111 -0
- package/template/client/index.html +1 -1
- package/template/client/package.json +1 -0
- package/template/client/src/app.tsx +40 -83
- package/template/client/src/main.tsx +2 -2
- package/template/client/src/monitor/components/MonitorCard.tsx +50 -0
- package/template/client/src/monitor/components/index.ts +1 -0
- package/template/client/src/monitor/hooks/index.ts +3 -0
- package/template/client/src/monitor/hooks/useCheck.mutation.ts +16 -0
- package/template/client/src/monitor/hooks/useMonitor.query.ts +64 -0
- package/template/client/src/monitor/index.ts +6 -0
- package/template/client/src/monitor/monitor.api.ts +11 -0
- package/template/client/src/monitor/monitor.mappers.ts +38 -0
- package/template/client/src/monitor/monitor.types.ts +23 -0
- package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +38 -0
- package/template/client/src/monitor/ui/StatusBadge.UI.tsx +14 -0
- package/template/client/src/monitor/ui/index.ts +8 -0
- package/template/client/src/shell/client.ts +67 -0
- package/template/client/src/shell/index.ts +20 -0
- package/template/client/src/shell/invoke.ts +35 -0
- package/template/client/src/shell/transformers.ts +72 -0
- package/template/client/src/shell/use-async.ts +56 -0
- package/template/client/src/shell/use-capability.ts +61 -0
- package/template/client/src/shell/use-node.ts +61 -0
- package/template/client/src/shell/use-shell.ts +91 -0
- package/template/client/src/shell/view-router.tsx +98 -0
- package/template/client/src/styles.css +177 -4
- package/template/client/src/ui/format.ts +24 -0
- package/template/client/src/ui/index.ts +9 -0
- package/template/client/src/ui/surface.tsx +56 -0
- package/template/client/src/ui/value.tsx +32 -0
- package/template/client/src/views/monitor.tsx +30 -0
- package/template/client/tsconfig.json +2 -1
- package/template/client/vite.config.ts +12 -13
- package/template/client/vitest.config.ts +12 -5
- package/template/core/monitor/health.ts +19 -0
- package/template/core/monitor/index.ts +9 -0
- package/template/core/monitor/keys.ts +29 -0
- package/template/core/monitor/node.ts +51 -0
- package/template/deps.ts +8 -8
- package/template/domain.ts +1 -1
- package/template/env.ts +2 -9
- package/template/integrations/prober/http.ts +43 -0
- package/template/integrations/prober/mock.ts +22 -0
- package/template/integrations/prober/port.ts +28 -0
- package/template/integrations/prober/registry.ts +66 -0
- package/template/package.json +1 -1
- package/template/pnpm-lock.yaml +2766 -0
- package/template/runtime/index.ts +36 -19
- package/template/runtime/monitor/check.ts +29 -0
- package/template/runtime/monitor/dependsOn.ts +16 -0
- package/template/runtime/monitor/index.ts +12 -0
- package/template/runtime/monitor/seed.ts +74 -0
- package/template/runtime/monitor/shared.ts +17 -0
- package/template/runtime/monitor/watch.ts +37 -0
- package/template/schema/index.ts +11 -4
- package/template/schema/monitor.ts +80 -0
- package/template/views/index.ts +9 -2
- package/template/views/monitor.ts +22 -0
- package/template/client/src/lib/kernel.ts +0 -135
- package/template/client/src/lib/shell.ts +0 -197
- package/template/client/src/lib/use-node.ts +0 -66
- package/template/client/src/lib/use-shell.ts +0 -85
- package/template/core/keys.ts +0 -28
- package/template/core/note.ts +0 -148
- package/template/integrations/summary/heuristic.ts +0 -25
- package/template/integrations/summary/http.ts +0 -69
- package/template/integrations/summary/port.ts +0 -21
- package/template/integrations/summary/registry.ts +0 -52
- package/template/schema/note.ts +0 -67
- package/template/views/note.ts +0 -21
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime composition root — the ONLY place request context (kernel/self/params/
|
|
3
3
|
* auth) and `deps` meet the per-feature logic. Each `execute` resolves the port
|
|
4
|
-
* it needs (`deps.
|
|
5
|
-
*
|
|
4
|
+
* it needs (`deps.prober()`, on-request) and delegates to the per-operation I/O
|
|
5
|
+
* functions in `runtime/monitor/` (`watch`/`check`/`dependsOn`/`seed`), which in
|
|
6
|
+
* turn call `core/` for pure decisions. No business logic lives here.
|
|
6
7
|
*
|
|
7
|
-
* - `
|
|
8
|
-
* - `
|
|
8
|
+
* - `watch` is interface-hosted (static) → `remoteInterfaceMethods`.
|
|
9
|
+
* - `check` is class-hosted (instance) → `remoteMethod` + `remoteClassMethods`.
|
|
10
|
+
* - `dependsOn` is class-hosted (instance).
|
|
9
11
|
* - `seed` is class-hosted (static) — the `postInstall` bootstrap.
|
|
10
12
|
*
|
|
11
13
|
* SDK-level `authorize` is an additive throw-to-deny check (returns void). For
|
|
@@ -20,43 +22,58 @@ import {
|
|
|
20
22
|
type SchemaMethodsImpl,
|
|
21
23
|
} from '@astrale-os/sdk'
|
|
22
24
|
|
|
23
|
-
import { createNote, reference, seed } from '../core/note'
|
|
24
25
|
import type { Deps } from '../deps'
|
|
26
|
+
|
|
27
|
+
import { MONITOR_KEYS } from '../core/monitor'
|
|
25
28
|
import { schema } from '../schema'
|
|
29
|
+
import { check, dependsOn, seed, watch } from './monitor'
|
|
26
30
|
|
|
27
31
|
const method = remoteMethod<Deps>()
|
|
28
32
|
const interfaceMethods = remoteInterfaceMethods<Deps>()
|
|
29
33
|
const classMethods = remoteClassMethods<Deps>()
|
|
30
34
|
|
|
31
|
-
const
|
|
32
|
-
|
|
35
|
+
const MonitorOpsMethods = interfaceMethods(schema, 'MonitorOps', {
|
|
36
|
+
watch: {
|
|
33
37
|
authorize: async () => undefined,
|
|
34
|
-
execute: ({ kernel, params
|
|
35
|
-
|
|
36
|
-
return createNote(kernel, deps.summarizer(), params)
|
|
38
|
+
execute: ({ kernel, params }) => {
|
|
39
|
+
return watch(kernel, params)
|
|
37
40
|
},
|
|
38
41
|
},
|
|
39
42
|
})
|
|
40
43
|
|
|
41
|
-
const
|
|
44
|
+
const checkMethod = method(schema, 'Monitor', 'check', {
|
|
45
|
+
authorize: async () => undefined,
|
|
46
|
+
execute: async ({ kernel, self, deps }) => {
|
|
47
|
+
if (!kernel) throw new Error('check requires a kernel credential')
|
|
48
|
+
const node = await self.node()
|
|
49
|
+
const url = node.props[MONITOR_KEYS.url]
|
|
50
|
+
if (typeof url !== 'string') throw new Error('Monitor node is missing its url property')
|
|
51
|
+
const prober = deps.prober({ class: node.class.raw, url })
|
|
52
|
+
return check(kernel, prober, self.path.raw, url)
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const dependsOnMethod = method(schema, 'Monitor', 'dependsOn', {
|
|
42
57
|
authorize: async () => undefined,
|
|
43
58
|
execute: ({ kernel, self, params }) => {
|
|
44
|
-
|
|
45
|
-
return reference(kernel, self.path.raw, params)
|
|
59
|
+
return dependsOn(kernel, self.path.raw, params)
|
|
46
60
|
},
|
|
47
61
|
})
|
|
48
62
|
|
|
49
|
-
const seedMethod = method(schema, '
|
|
63
|
+
const seedMethod = method(schema, 'Monitor', 'seed', {
|
|
50
64
|
authorize: async () => undefined,
|
|
51
65
|
execute: ({ kernel, deps }) => {
|
|
52
|
-
|
|
53
|
-
return seed(kernel, deps.summarizer())
|
|
66
|
+
return seed(kernel, deps.prober())
|
|
54
67
|
},
|
|
55
68
|
})
|
|
56
69
|
|
|
57
|
-
const
|
|
70
|
+
const MonitorMethods = classMethods(schema, 'Monitor', {
|
|
71
|
+
check: checkMethod,
|
|
72
|
+
dependsOn: dependsOnMethod,
|
|
73
|
+
seed: seedMethod,
|
|
74
|
+
})
|
|
58
75
|
|
|
59
76
|
export const methods: SchemaMethodsImpl<typeof schema, Deps> = {
|
|
60
|
-
interface: {
|
|
61
|
-
class: {
|
|
77
|
+
interface: { MonitorOps: MonitorOpsMethods },
|
|
78
|
+
class: { Monitor: MonitorMethods },
|
|
62
79
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Monitor.check` (instance) — probe the target via the resolved `Prober` port,
|
|
3
|
+
* classify the result (pure, `core/health`), and record status/latency back onto
|
|
4
|
+
* the node. `url` is the monitor's target and `prober` is already selected FOR
|
|
5
|
+
* this node (both resolved by the wiring in `runtime/index.ts` from `self.node()`).
|
|
6
|
+
*/
|
|
7
|
+
import { classify, MONITOR_KEYS } from '../../core/monitor'
|
|
8
|
+
import type { Prober } from '../../integrations/prober/port'
|
|
9
|
+
|
|
10
|
+
import type { CallableKernel } from './shared'
|
|
11
|
+
|
|
12
|
+
export async function check(
|
|
13
|
+
kernel: CallableKernel,
|
|
14
|
+
prober: Prober,
|
|
15
|
+
selfPathRaw: string,
|
|
16
|
+
url: string,
|
|
17
|
+
): Promise<{ status: string; statusCode: number; latencyMs: number }> {
|
|
18
|
+
const result = await prober.probe(url)
|
|
19
|
+
const status = classify(result.statusCode)
|
|
20
|
+
await kernel.call(`${selfPathRaw}::update`, {
|
|
21
|
+
props: {
|
|
22
|
+
[MONITOR_KEYS.status]: status,
|
|
23
|
+
[MONITOR_KEYS.statusCode]: result.statusCode,
|
|
24
|
+
[MONITOR_KEYS.latencyMs]: result.latencyMs,
|
|
25
|
+
[MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
return { status, statusCode: result.statusCode, latencyMs: result.latencyMs }
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Monitor.dependsOn` (instance) — link this Monitor to another it relies on via
|
|
3
|
+
* a `depends_on` edge. `target` is any node address (a tree path or `@<id>`).
|
|
4
|
+
*/
|
|
5
|
+
import { DEPENDS_ON_EDGE } from '../../core/monitor'
|
|
6
|
+
|
|
7
|
+
import type { CallableKernel } from './shared'
|
|
8
|
+
|
|
9
|
+
export async function dependsOn(
|
|
10
|
+
kernel: CallableKernel,
|
|
11
|
+
selfPathRaw: string,
|
|
12
|
+
params: { target: string },
|
|
13
|
+
): Promise<{ linked: string }> {
|
|
14
|
+
await kernel.call(`${selfPathRaw}::link`, { edgeClass: DEPENDS_ON_EDGE, target: params.target })
|
|
15
|
+
return { linked: params.target }
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor runtime operations — one file per method, assembled here. The
|
|
3
|
+
* composition root (`runtime/index.ts`) imports these and wires them into the
|
|
4
|
+
* methods map. Each is transport-agnostic logic over a `CallableKernel` (+ the
|
|
5
|
+
* `Prober` port where it probes), delegating pure decisions to `core/` — so all
|
|
6
|
+
* are unit-testable with fakes.
|
|
7
|
+
*/
|
|
8
|
+
export { check } from './check'
|
|
9
|
+
export { dependsOn } from './dependsOn'
|
|
10
|
+
export { seed } from './seed'
|
|
11
|
+
export { watch } from './watch'
|
|
12
|
+
export type { CallableKernel } from './shared'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Monitor.seed` (static) — the domain's post-install bootstrap. The kernel calls
|
|
3
|
+
* it ONCE after install, as __SYSTEM__ (see `postInstall` in `domain.ts`), so the
|
|
4
|
+
* domain can lay down its initial state: the `/monitors` folder, a couple of
|
|
5
|
+
* starter Monitors (each probed once so they show real status immediately), and a
|
|
6
|
+
* `depends_on` edge between them. Idempotent: a re-run swallows `PATH_CONFLICT`
|
|
7
|
+
* per node (the starters use fixed slugs).
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
classify,
|
|
11
|
+
DEPENDS_ON_EDGE,
|
|
12
|
+
FOLDER_CLASS,
|
|
13
|
+
MONITOR_CLASS,
|
|
14
|
+
MONITOR_KEYS,
|
|
15
|
+
MONITORS_PARENT,
|
|
16
|
+
NAME_KEY,
|
|
17
|
+
NODE_CREATE,
|
|
18
|
+
STARTERS,
|
|
19
|
+
} from '../../core/monitor'
|
|
20
|
+
import type { Prober } from '../../integrations/prober/port'
|
|
21
|
+
|
|
22
|
+
import { type CallableKernel, isPathConflict } from './shared'
|
|
23
|
+
|
|
24
|
+
export async function seed(kernel: CallableKernel, prober: Prober): Promise<{ seeded: number }> {
|
|
25
|
+
// 1. The `/monitors` folder at the graph root.
|
|
26
|
+
try {
|
|
27
|
+
await kernel.call(NODE_CREATE, {
|
|
28
|
+
class: FOLDER_CLASS,
|
|
29
|
+
path: MONITORS_PARENT,
|
|
30
|
+
props: { [NAME_KEY]: 'monitors' },
|
|
31
|
+
})
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (!isPathConflict(e)) throw e
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. A couple of starter Monitors, each probed once for an initial status.
|
|
37
|
+
const ids: Record<string, string> = {}
|
|
38
|
+
let seeded = 0
|
|
39
|
+
for (const s of STARTERS) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await prober.probe(s.url)
|
|
42
|
+
const created = (await kernel.call(NODE_CREATE, {
|
|
43
|
+
class: MONITOR_CLASS,
|
|
44
|
+
path: `${MONITORS_PARENT}/${s.slug}`,
|
|
45
|
+
props: {
|
|
46
|
+
[NAME_KEY]: s.name,
|
|
47
|
+
[MONITOR_KEYS.url]: s.url,
|
|
48
|
+
[MONITOR_KEYS.status]: classify(result.statusCode),
|
|
49
|
+
[MONITOR_KEYS.statusCode]: result.statusCode,
|
|
50
|
+
[MONITOR_KEYS.latencyMs]: result.latencyMs,
|
|
51
|
+
[MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
|
|
52
|
+
},
|
|
53
|
+
})) as { id: string }
|
|
54
|
+
ids[s.slug] = created.id
|
|
55
|
+
seeded++
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (!isPathConflict(e)) throw e
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Link the first starter as depending on the second (id-form on both sides
|
|
62
|
+
// — layout-independent). Best-effort.
|
|
63
|
+
const from = ids[STARTERS[0]?.slug ?? '']
|
|
64
|
+
const to = ids[STARTERS[1]?.slug ?? '']
|
|
65
|
+
if (from && to) {
|
|
66
|
+
try {
|
|
67
|
+
await kernel.call(`@${from}::link`, { edgeClass: DEPENDS_ON_EDGE, target: `@${to}` })
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (!isPathConflict(e)) throw e
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { seeded }
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-internal helpers shared across the Monitor operations (`watch`,
|
|
3
|
+
* `check`, `dependsOn`, `seed`). Not exported from the domain — just the seam the
|
|
4
|
+
* operation files agree on.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The minimal kernel surface the operations need — kept narrow so each is
|
|
9
|
+
* unit-testable with a fake `{ call }`. The SDK hands the real client at the
|
|
10
|
+
* `runtime/index.ts` seam.
|
|
11
|
+
*/
|
|
12
|
+
export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
|
|
13
|
+
|
|
14
|
+
/** True for the kernel's create-collision error — lets `seed` stay idempotent. */
|
|
15
|
+
export function isPathConflict(e: unknown): boolean {
|
|
16
|
+
return e instanceof Error && e.message.includes('PATH_CONFLICT')
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CallableKernel } from './shared'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `MonitorOps.watch` (static) — create a Monitor node under `/monitors`. The slug
|
|
5
|
+
* FORMAT is pure (`core/monitor`'s `monitorSlug`); the entropy suffix (impure,
|
|
6
|
+
* clock/RNG) is generated HERE, keeping core deterministic.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
MONITOR_CLASS,
|
|
10
|
+
MONITOR_KEYS,
|
|
11
|
+
MONITORS_PARENT,
|
|
12
|
+
monitorSlug,
|
|
13
|
+
NAME_KEY,
|
|
14
|
+
NODE_CREATE,
|
|
15
|
+
} from '../../core/monitor'
|
|
16
|
+
|
|
17
|
+
/** Short clock+RNG entropy to dodge same-ms / same-host slug collisions. */
|
|
18
|
+
function randomSuffix(): string {
|
|
19
|
+
return `${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function watch(
|
|
23
|
+
kernel: CallableKernel,
|
|
24
|
+
params: { url: string; name?: string },
|
|
25
|
+
): Promise<{ id: string; path: string }> {
|
|
26
|
+
const path = `${MONITORS_PARENT}/${monitorSlug(params.url, randomSuffix())}`
|
|
27
|
+
const created = (await kernel.call(NODE_CREATE, {
|
|
28
|
+
class: MONITOR_CLASS,
|
|
29
|
+
path,
|
|
30
|
+
props: {
|
|
31
|
+
[NAME_KEY]: params.name ?? params.url,
|
|
32
|
+
[MONITOR_KEYS.url]: params.url,
|
|
33
|
+
[MONITOR_KEYS.status]: 'unknown',
|
|
34
|
+
},
|
|
35
|
+
})) as { id: string }
|
|
36
|
+
return { id: created.id, path }
|
|
37
|
+
}
|
package/template/schema/index.ts
CHANGED
|
@@ -11,13 +11,20 @@
|
|
|
11
11
|
import { defineSchema, KernelSchema } from '@astrale-os/kernel-core'
|
|
12
12
|
import { compileDomain, type Domain } from '@astrale-os/kernel-core/domain'
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { depends_on, Monitor, MonitorOps } from './monitor'
|
|
15
15
|
|
|
16
16
|
export const schema = defineSchema('astrale-domain.example.dev', {
|
|
17
|
-
interfaces: {
|
|
18
|
-
classes: {
|
|
17
|
+
interfaces: { MonitorOps },
|
|
18
|
+
classes: { Monitor, depends_on },
|
|
19
19
|
imports: [KernelSchema],
|
|
20
20
|
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The compiled domain — resolved class/interface paths + qualified prop keys.
|
|
24
|
+
* `compileDomain` is pure (no env), so this is safe to import from the worker
|
|
25
|
+
* and from build tooling alike. `core/keys.ts` re-exports it as the single source
|
|
26
|
+
* of graph-facing strings (class paths, prop keys) — never hand-write those.
|
|
27
|
+
*/
|
|
21
28
|
export const D: Domain<typeof schema> = compileDomain(schema)
|
|
22
29
|
|
|
23
|
-
export * from './
|
|
30
|
+
export * from './monitor'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor context — the domain's single bounded slice.
|
|
3
|
+
*
|
|
4
|
+
* One file per context: a class (or a few tightly-related classes) plus the
|
|
5
|
+
* edges that bind them. Here that is the `MonitorOps` interface, the `Monitor`
|
|
6
|
+
* class that implements it, and the `depends_on` edge from one Monitor to
|
|
7
|
+
* another. To grow the domain, add `schema/<context>.ts` and register its
|
|
8
|
+
* members in `schema/index.ts`.
|
|
9
|
+
*
|
|
10
|
+
* - Interface `MonitorOps` one static op, `watch`. Static → the impl gets no
|
|
11
|
+
* `self`; it creates a brand-new Monitor.
|
|
12
|
+
* - Class `Monitor` implements `[MonitorOps, Container]`, inheriting
|
|
13
|
+
* `watch` and adding instance methods `check`
|
|
14
|
+
* (probe + record live status) and `dependsOn`
|
|
15
|
+
* (links this Monitor to one it relies on).
|
|
16
|
+
* - Edge `depends_on` Monitor → Monitor. A dependency graph of checks,
|
|
17
|
+
* materialized by `dependsOn` (and by `seed`).
|
|
18
|
+
*/
|
|
19
|
+
import { edgeClass, KernelSchema, nodeClass, nodeInterface } from '@astrale-os/kernel-core'
|
|
20
|
+
import { fn } from '@astrale-os/kernel-dsl'
|
|
21
|
+
import { z } from 'zod'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Thin ref to a created node — what node-creating ops return. A remote method
|
|
25
|
+
* returns a plain `{ id, path }`, never `ref(SELF)` (whose full-Node value does
|
|
26
|
+
* not round-trip over the worker wire).
|
|
27
|
+
*/
|
|
28
|
+
export const MonitorRef = z.object({ id: z.string(), path: z.string() })
|
|
29
|
+
|
|
30
|
+
/** What a single probe records — returned by `check`. */
|
|
31
|
+
export const ProbeOutcome = z.object({
|
|
32
|
+
status: z.string(),
|
|
33
|
+
statusCode: z.number().int(),
|
|
34
|
+
latencyMs: z.number().int(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const MonitorOps = nodeInterface({
|
|
38
|
+
methods: {
|
|
39
|
+
watch: fn({
|
|
40
|
+
static: true,
|
|
41
|
+
params: { url: z.string(), name: z.string().optional() },
|
|
42
|
+
returns: MonitorRef,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export const Monitor = nodeClass({
|
|
48
|
+
implements: [MonitorOps, KernelSchema.interfaces.Container],
|
|
49
|
+
props: {
|
|
50
|
+
/** The target URL this monitor probes. */
|
|
51
|
+
url: z.string(),
|
|
52
|
+
// Live status, written by `check`. PLAIN STRING, not `z.enum()`: `::update`
|
|
53
|
+
// currently drops `z.enum()` props silently, and `check` updates this.
|
|
54
|
+
// Values: 'up' | 'down' | 'unknown' (the initial value before the first check).
|
|
55
|
+
status: z.string().optional(),
|
|
56
|
+
/** Last observed HTTP status code (0 = host unreachable). */
|
|
57
|
+
statusCode: z.number().int().optional(),
|
|
58
|
+
/** Last observed round-trip latency, in ms. */
|
|
59
|
+
latencyMs: z.number().int().optional(),
|
|
60
|
+
/** ISO timestamp of the last check. */
|
|
61
|
+
lastCheckedAt: z.string().optional(),
|
|
62
|
+
},
|
|
63
|
+
methods: {
|
|
64
|
+
// Instance: probe the target via the resolved `Prober` port (the external
|
|
65
|
+
// integration), then record status/latency back onto this node.
|
|
66
|
+
check: fn({ returns: ProbeOutcome }),
|
|
67
|
+
// Instance: link this Monitor to another it depends on (a `depends_on` edge).
|
|
68
|
+
dependsOn: fn({ params: { target: z.string() }, returns: z.object({ linked: z.string() }) }),
|
|
69
|
+
// Post-install bootstrap (wired as `postInstall` in domain.ts). Static: the
|
|
70
|
+
// kernel calls it ONCE after install, as __SYSTEM__, with no `self`. Must
|
|
71
|
+
// stay idempotent — a re-install runs it again.
|
|
72
|
+
seed: fn({ static: true, returns: z.object({ seeded: z.number().int() }) }),
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const depends_on = edgeClass(
|
|
77
|
+
{ as: 'dependent', types: [Monitor] },
|
|
78
|
+
{ as: 'dependency', types: [Monitor] },
|
|
79
|
+
{ props: { reason: z.string().optional() } },
|
|
80
|
+
)
|
package/template/views/index.ts
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
* key; each becomes a View node at `/<origin>/core/views/<slug>` whose iframe
|
|
4
4
|
* binding the SDK stamps with the worker's live serving URL when it builds the
|
|
5
5
|
* install bundle.
|
|
6
|
+
*
|
|
7
|
+
* `welcome` is a worker-rendered inline-HTML view (no SPA). `ui-monitor` and
|
|
8
|
+
* `ui-monitor-badge` are BOTH served by the one `client/` SPA — it routes on the
|
|
9
|
+
* mount path (`/ui/monitor` vs `/ui/monitor-badge`) to pick which view to render.
|
|
6
10
|
*/
|
|
7
|
-
import {
|
|
11
|
+
import { monitor } from './monitor'
|
|
8
12
|
import { welcome } from './welcome'
|
|
9
13
|
|
|
10
|
-
export const views = {
|
|
14
|
+
export const views = {
|
|
15
|
+
welcome,
|
|
16
|
+
'ui-monitor': monitor,
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ui-monitor` — a rich View backed by the `client/` React + Vite SPA (instead of
|
|
3
|
+
* an inline-HTML `render` like `welcome`). Because it declares `mount` rather
|
|
4
|
+
* than `render`, the View node's iframe binding points at
|
|
5
|
+
* `<serving url>/ui/monitor` — the SDK stamps it from the worker's live URL when
|
|
6
|
+
* it builds the install bundle. The Cloudflare adapter serves `/ui/*` from
|
|
7
|
+
* `../.dist` (built by `client/` with base `/ui/`) via the Worker's `ASSETS`
|
|
8
|
+
* binding.
|
|
9
|
+
*
|
|
10
|
+
* `viewFor: selfOf(Monitor)` attaches a `view_for` edge to the `Monitor` class
|
|
11
|
+
* meta-node, so the GUI offers this view for any Monitor instance.
|
|
12
|
+
*/
|
|
13
|
+
import { selfOf } from '@astrale-os/kernel-dsl'
|
|
14
|
+
import { defineView } from '@astrale-os/sdk'
|
|
15
|
+
|
|
16
|
+
import { Monitor } from '../schema/monitor'
|
|
17
|
+
|
|
18
|
+
export const monitor = defineView({
|
|
19
|
+
auth: 'public',
|
|
20
|
+
mount: '/ui/monitor',
|
|
21
|
+
viewFor: selfOf(Monitor),
|
|
22
|
+
})
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal, self-contained kernel JSON client — just enough to load a node via
|
|
3
|
-
* `@<id>::get`, without pulling in `@astrale-os/kernel-client` (and its
|
|
4
|
-
* transitive deps). It reproduces the kernel ENVELOPE wire shape inline.
|
|
5
|
-
*
|
|
6
|
-
* Wire contract (authoritative source: `kernel/api/envelope/`):
|
|
7
|
-
* - Request: POST <kernelUrl> with headers
|
|
8
|
-
* content-type: application/vnd.astrale.kernel+json
|
|
9
|
-
* accept: application/vnd.astrale.kernel+json
|
|
10
|
-
* authorization: <delegationToken> (BARE token — no "Bearer " prefix)
|
|
11
|
-
* body JSON `{ method, params, id }` (see `encode.ts:encodeKernelRequest`).
|
|
12
|
-
* - Response: JSON, exactly one of (decode precedence error → redirect → result,
|
|
13
|
-
* mirroring `decode.ts:decodeKernelResponse`):
|
|
14
|
-
* { error: { code, message }, id } → throw Error("<code>: <message>")
|
|
15
|
-
* { redirect, id } → throw (we don't follow redirects here)
|
|
16
|
-
* { result, id } → return result
|
|
17
|
-
*
|
|
18
|
-
* Deliberately JSON-only: no msgpack codec, no streaming/binary, no redirect
|
|
19
|
-
* following, no schema/batching. Those live in `@astrale-os/kernel-client`,
|
|
20
|
-
* which this self-contained build omits on purpose.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
// From `kernel/api/envelope/types.ts` (KERNEL_CONTENT_TYPE_JSON). Inlined so the
|
|
24
|
-
// template needs no @astrale-os import.
|
|
25
|
-
const KERNEL_CONTENT_TYPE_JSON = 'application/vnd.astrale.kernel+json'
|
|
26
|
-
|
|
27
|
-
// Module-local monotonic request id. Strings keep ids stable across reloads and
|
|
28
|
-
// distinguishable in logs; the kernel echoes `id` back but we don't correlate
|
|
29
|
-
// (one request per fetch).
|
|
30
|
-
let idSeq = 0
|
|
31
|
-
function nextId(): string {
|
|
32
|
-
idSeq += 1
|
|
33
|
-
return `c${idSeq}`
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Prop key constants — the graph stores props with fully-qualified keys
|
|
38
|
-
* (`<domain>:<member>.property.<name>`). The kernel `Named.name` key is fixed
|
|
39
|
-
* and known; domain props (`title`, `body`) are qualified by the (build-time
|
|
40
|
-
* unknown) domain origin — read those by suffix with `readPropBySuffix`.
|
|
41
|
-
*/
|
|
42
|
-
export const PROP = {
|
|
43
|
-
named: {
|
|
44
|
-
name: 'kernel.astrale.ai:interface.Named.property.name',
|
|
45
|
-
},
|
|
46
|
-
} as const
|
|
47
|
-
|
|
48
|
-
export type KernelNode = {
|
|
49
|
-
id: string
|
|
50
|
-
path: string
|
|
51
|
-
class: string | { raw?: string }
|
|
52
|
-
props: Record<string, unknown>
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function readProp(props: Record<string, unknown>, key: string): string | undefined {
|
|
56
|
-
const v = props[key]
|
|
57
|
-
return typeof v === 'string' ? v : undefined
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Read a string prop by key suffix — for domain-qualified props whose full key
|
|
62
|
-
* embeds the (build-time-unknown) domain origin. e.g.
|
|
63
|
-
* `readPropBySuffix(props, '.property.body')`.
|
|
64
|
-
*/
|
|
65
|
-
export function readPropBySuffix(
|
|
66
|
-
props: Record<string, unknown>,
|
|
67
|
-
suffix: string,
|
|
68
|
-
): string | undefined {
|
|
69
|
-
for (const [k, v] of Object.entries(props)) {
|
|
70
|
-
if (k.endsWith(suffix) && typeof v === 'string') return v
|
|
71
|
-
}
|
|
72
|
-
return undefined
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Short class name from a `class.raw` path `/:<domain>:class.<Name>` → `<Name>`. */
|
|
76
|
-
export function classShortName(node: KernelNode): string {
|
|
77
|
-
const raw = (typeof node.class === 'string' ? node.class : node.class?.raw) ?? ''
|
|
78
|
-
const last = raw.split(':').pop() ?? ''
|
|
79
|
-
const dot = last.indexOf('.')
|
|
80
|
-
return dot >= 0 ? last.slice(dot + 1) : last
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* POST a single kernel call and return its `result`. Throws on a kernel error
|
|
85
|
-
* envelope or an unexpected redirect. `token` is the bare delegation credential
|
|
86
|
-
* from the shell handshake; the iframe authenticates SOLELY with it (the parent
|
|
87
|
-
* minted it for this kernel, so the audience already matches — no cookie/mint).
|
|
88
|
-
*/
|
|
89
|
-
export async function kernelCall(
|
|
90
|
-
kernelUrl: string,
|
|
91
|
-
token: string,
|
|
92
|
-
method: string,
|
|
93
|
-
params: Record<string, unknown> = {},
|
|
94
|
-
): Promise<unknown> {
|
|
95
|
-
// The kernel routes on a trailing slash; the parent absolutizes the URL, but
|
|
96
|
-
// not always with the slash, so normalize here.
|
|
97
|
-
const url = kernelUrl.endsWith('/') ? kernelUrl : `${kernelUrl}/`
|
|
98
|
-
const id = nextId()
|
|
99
|
-
|
|
100
|
-
const res = await fetch(url, {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: {
|
|
103
|
-
'content-type': KERNEL_CONTENT_TYPE_JSON,
|
|
104
|
-
accept: KERNEL_CONTENT_TYPE_JSON,
|
|
105
|
-
authorization: token,
|
|
106
|
-
},
|
|
107
|
-
body: JSON.stringify({ method, params, id }),
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
let body: unknown
|
|
111
|
-
try {
|
|
112
|
-
body = await res.json()
|
|
113
|
-
} catch {
|
|
114
|
-
throw new Error(`kernel returned a non-JSON response (HTTP ${res.status})`)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (body === null || typeof body !== 'object') {
|
|
118
|
-
throw new Error(`kernel returned an unexpected response (HTTP ${res.status})`)
|
|
119
|
-
}
|
|
120
|
-
const obj = body as Record<string, unknown>
|
|
121
|
-
|
|
122
|
-
// Decode precedence error → redirect → result (mirrors decodeKernelResponse).
|
|
123
|
-
if ('error' in obj && obj.error && typeof obj.error === 'object') {
|
|
124
|
-
const err = obj.error as { code?: unknown; message?: unknown }
|
|
125
|
-
const code = typeof err.code === 'number' ? err.code : 5000
|
|
126
|
-
const message = typeof err.message === 'string' ? err.message : 'Unknown error'
|
|
127
|
-
throw new Error(`${code}: ${message}`)
|
|
128
|
-
}
|
|
129
|
-
if ('redirect' in obj && obj.redirect) {
|
|
130
|
-
// Redirects (remote-domain Functions) aren't followed by this minimal
|
|
131
|
-
// client — they require credential re-minting against the target worker.
|
|
132
|
-
throw new Error('unexpected redirect from kernel (not supported by the template client)')
|
|
133
|
-
}
|
|
134
|
-
return obj.result
|
|
135
|
-
}
|