@astrale-os/adapter-cloudflare 0.1.10 → 0.2.0
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/client/README.md +25 -23
- package/template/client/__tests__/app.test.tsx +44 -88
- package/template/client/__tests__/harness.ts +11 -16
- package/template/client/__tests__/kernel.test.ts +9 -35
- package/template/client/__tests__/seam.test.tsx +22 -18
- package/template/client/src/app.tsx +11 -17
- package/template/client/src/shell/use-capability.ts +1 -3
- package/template/client/src/shell/use-node.ts +2 -2
- package/template/client/src/shell/view-router.tsx +3 -4
- package/template/client/src/status/components/StatusCard.tsx +50 -0
- package/template/client/src/status/components/index.ts +1 -0
- package/template/client/src/status/hooks/index.ts +3 -0
- package/template/client/src/{monitor → status}/hooks/useCheck.mutation.ts +2 -2
- package/template/client/src/{monitor/hooks/useMonitor.query.ts → status/hooks/useCheckable.query.ts} +8 -8
- package/template/client/src/status/index.ts +7 -0
- package/template/client/src/status/status.api.ts +12 -0
- package/template/client/src/status/status.mappers.ts +19 -0
- package/template/client/src/status/status.types.ts +11 -0
- package/template/client/src/styles.css +5 -0
- package/template/client/src/ui/StatusBadge.tsx +31 -0
- package/template/client/src/ui/index.ts +6 -2
- package/template/client/src/views/status.tsx +28 -0
- package/template/client/vite.config.ts +2 -3
- package/template/client/vitest.config.ts +1 -2
- package/template/core/monitor/health.ts +19 -4
- package/template/core/monitor/keys.ts +14 -2
- package/template/core/monitor/node.ts +27 -21
- package/template/deps.ts +2 -1
- package/template/integrations/prober/http.ts +4 -15
- package/template/integrations/prober/mock.ts +1 -5
- package/template/integrations/prober/port.ts +0 -2
- package/template/integrations/prober/registry.ts +6 -7
- package/template/package.json +1 -1
- package/template/runtime/index.ts +51 -39
- package/template/runtime/monitor/check.ts +9 -9
- package/template/runtime/monitor/index.ts +4 -7
- package/template/runtime/monitor/seed.ts +67 -46
- package/template/runtime/monitor/watch.ts +6 -12
- package/template/runtime/{monitor/shared.ts → shared.ts} +7 -3
- package/template/runtime/status-page/add.ts +21 -0
- package/template/runtime/status-page/check.ts +50 -0
- package/template/runtime/status-page/create.ts +24 -0
- package/template/runtime/status-page/index.ts +8 -0
- package/template/schema/index.ts +5 -5
- package/template/schema/monitor.ts +62 -48
- package/template/views/index.ts +4 -5
- package/template/views/status-page.ts +16 -0
- package/template/client/src/monitor/components/MonitorCard.tsx +0 -50
- package/template/client/src/monitor/components/index.ts +0 -1
- package/template/client/src/monitor/hooks/index.ts +0 -3
- package/template/client/src/monitor/index.ts +0 -6
- package/template/client/src/monitor/monitor.api.ts +0 -11
- package/template/client/src/monitor/monitor.mappers.ts +0 -38
- package/template/client/src/monitor/monitor.types.ts +0 -23
- package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +0 -38
- package/template/client/src/monitor/ui/StatusBadge.UI.tsx +0 -14
- package/template/client/src/monitor/ui/index.ts +0 -8
- package/template/client/src/views/monitor.tsx +0 -30
- package/template/runtime/monitor/dependsOn.ts +0 -16
- package/template/views/monitor.ts +0 -22
|
@@ -1,62 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime composition root — the ONLY place request context (kernel/self/params/
|
|
3
|
-
* auth) and `deps` meet the per-feature logic. Each `execute` resolves
|
|
4
|
-
*
|
|
5
|
-
* functions in `runtime/monitor/`
|
|
6
|
-
*
|
|
3
|
+
* auth) and `deps` meet the per-feature logic. Each `execute` resolves what it
|
|
4
|
+
* needs (the `Prober` port, the self node) and delegates to the per-operation I/O
|
|
5
|
+
* functions in `runtime/monitor/` and `runtime/status-page/`. No business logic
|
|
6
|
+
* lives here.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* `check` is the `Checkable` interface method, declared `abstract`, so it is
|
|
9
|
+
* implemented PER CLASS (Monitor probes; StatusPage rolls up) — wired under
|
|
10
|
+
* `class.Monitor` / `class.StatusPage`, NOT a shared `interface` block. The
|
|
11
|
+
* kernel dispatches `@<node>::check` to the impl for that node's class.
|
|
12
12
|
*
|
|
13
13
|
* SDK-level `authorize` is an additive throw-to-deny check (returns void). For
|
|
14
14
|
* finer worker checks see `assertPerm` / `requireOwnership` from `@astrale-os/sdk`.
|
|
15
|
-
*
|
|
16
|
-
* Exports the `methods` map the domain definition (`domain.ts`) wires in.
|
|
17
15
|
*/
|
|
18
|
-
import {
|
|
19
|
-
remoteClassMethods,
|
|
20
|
-
remoteInterfaceMethods,
|
|
21
|
-
remoteMethod,
|
|
22
|
-
type SchemaMethodsImpl,
|
|
23
|
-
} from '@astrale-os/sdk'
|
|
16
|
+
import { remoteClassMethods, remoteMethod, type SchemaMethodsImpl } from '@astrale-os/sdk'
|
|
24
17
|
|
|
25
18
|
import type { Deps } from '../deps'
|
|
26
19
|
|
|
27
20
|
import { MONITOR_KEYS } from '../core/monitor'
|
|
28
21
|
import { schema } from '../schema'
|
|
29
|
-
import { check
|
|
22
|
+
import { check as monitorCheck, seed, watch } from './monitor'
|
|
23
|
+
import { add, check as pageCheck, create } from './status-page'
|
|
30
24
|
|
|
31
25
|
const method = remoteMethod<Deps>()
|
|
32
|
-
const interfaceMethods = remoteInterfaceMethods<Deps>()
|
|
33
26
|
const classMethods = remoteClassMethods<Deps>()
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
watch: {
|
|
37
|
-
authorize: async () => undefined,
|
|
38
|
-
execute: ({ kernel, params }) => {
|
|
39
|
-
return watch(kernel, params)
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
})
|
|
28
|
+
// ── Monitor ─────────────────────────────────────────────────────────
|
|
43
29
|
|
|
44
|
-
const
|
|
30
|
+
const monitorCheckMethod = method(schema, 'Monitor', 'check', {
|
|
45
31
|
authorize: async () => undefined,
|
|
46
32
|
execute: async ({ kernel, self, deps }) => {
|
|
47
|
-
if (!kernel) throw new Error('check requires a kernel credential')
|
|
48
33
|
const node = await self.node()
|
|
49
34
|
const url = node.props[MONITOR_KEYS.url]
|
|
50
35
|
if (typeof url !== 'string') throw new Error('Monitor node is missing its url property')
|
|
51
|
-
|
|
52
|
-
return
|
|
36
|
+
// Pass the node to the picker — the prober is chosen FOR this monitor.
|
|
37
|
+
return monitorCheck(kernel, deps.prober({ class: node.class.raw, url }), self.path.raw, url)
|
|
53
38
|
},
|
|
54
39
|
})
|
|
55
40
|
|
|
56
|
-
const
|
|
41
|
+
const watchMethod = method(schema, 'Monitor', 'watch', {
|
|
57
42
|
authorize: async () => undefined,
|
|
58
|
-
execute: ({ kernel,
|
|
59
|
-
return
|
|
43
|
+
execute: ({ kernel, params }) => {
|
|
44
|
+
return watch(kernel, params)
|
|
60
45
|
},
|
|
61
46
|
})
|
|
62
47
|
|
|
@@ -67,13 +52,40 @@ const seedMethod = method(schema, 'Monitor', 'seed', {
|
|
|
67
52
|
},
|
|
68
53
|
})
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
// ── StatusPage ──────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const pageCheckMethod = method(schema, 'StatusPage', 'check', {
|
|
58
|
+
authorize: async () => undefined,
|
|
59
|
+
execute: ({ kernel, self }) => {
|
|
60
|
+
return pageCheck(kernel, self.path.raw)
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const createMethod = method(schema, 'StatusPage', 'create', {
|
|
65
|
+
authorize: async () => undefined,
|
|
66
|
+
execute: ({ kernel, params }) => {
|
|
67
|
+
return create(kernel, params)
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const addMethod = method(schema, 'StatusPage', 'add', {
|
|
72
|
+
authorize: async () => undefined,
|
|
73
|
+
execute: ({ kernel, self, params }) => {
|
|
74
|
+
return add(kernel, self.path.raw, params)
|
|
75
|
+
},
|
|
74
76
|
})
|
|
75
77
|
|
|
76
78
|
export const methods: SchemaMethodsImpl<typeof schema, Deps> = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
class: {
|
|
80
|
+
Monitor: classMethods(schema, 'Monitor', {
|
|
81
|
+
check: monitorCheckMethod,
|
|
82
|
+
watch: watchMethod,
|
|
83
|
+
seed: seedMethod,
|
|
84
|
+
}),
|
|
85
|
+
StatusPage: classMethods(schema, 'StatusPage', {
|
|
86
|
+
check: pageCheckMethod,
|
|
87
|
+
create: createMethod,
|
|
88
|
+
add: addMethod,
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
79
91
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
+
import type { Prober } from '../../integrations/prober/port'
|
|
2
|
+
import type { CallableKernel } from '../shared'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
* `Monitor.check` (instance) —
|
|
3
|
-
*
|
|
4
|
-
* the node
|
|
5
|
-
*
|
|
5
|
+
* `Monitor.check` (instance) — implements `Checkable.check` for a single endpoint:
|
|
6
|
+
* probe the target via the resolved `Prober` port, classify (pure, `core/monitor`),
|
|
7
|
+
* record status/latency on the node, and return the verdict. `url` + `prober` are
|
|
8
|
+
* resolved by the wiring (`runtime/index.ts`) from `self.node()`.
|
|
6
9
|
*/
|
|
7
10
|
import { classify, MONITOR_KEYS } from '../../core/monitor'
|
|
8
|
-
import type { Prober } from '../../integrations/prober/port'
|
|
9
|
-
|
|
10
|
-
import type { CallableKernel } from './shared'
|
|
11
11
|
|
|
12
12
|
export async function check(
|
|
13
13
|
kernel: CallableKernel,
|
|
14
14
|
prober: Prober,
|
|
15
15
|
selfPathRaw: string,
|
|
16
16
|
url: string,
|
|
17
|
-
): Promise<{ status: string
|
|
17
|
+
): Promise<{ status: string }> {
|
|
18
18
|
const result = await prober.probe(url)
|
|
19
19
|
const status = classify(result.statusCode)
|
|
20
20
|
await kernel.call(`${selfPathRaw}::update`, {
|
|
@@ -25,5 +25,5 @@ export async function check(
|
|
|
25
25
|
[MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
|
|
26
26
|
},
|
|
27
27
|
})
|
|
28
|
-
return { status
|
|
28
|
+
return { status }
|
|
29
29
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Monitor
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* are unit-testable with fakes.
|
|
2
|
+
* Monitor operations — one file per method, assembled here for the composition
|
|
3
|
+
* root (`runtime/index.ts`). Each is transport-agnostic logic over a
|
|
4
|
+
* `CallableKernel` (+ the `Prober` port where it probes), delegating pure
|
|
5
|
+
* decisions to `core/monitor` — so all are unit-testable with fakes.
|
|
7
6
|
*/
|
|
8
7
|
export { check } from './check'
|
|
9
|
-
export { dependsOn } from './dependsOn'
|
|
10
8
|
export { seed } from './seed'
|
|
11
9
|
export { watch } from './watch'
|
|
12
|
-
export type { CallableKernel } from './shared'
|
|
@@ -1,73 +1,94 @@
|
|
|
1
|
+
import type { Prober } from '../../integrations/prober/port'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* `Monitor.seed` (static) — the domain's post-install bootstrap. The kernel calls
|
|
3
5
|
* it ONCE after install, as __SYSTEM__ (see `postInstall` in `domain.ts`), so the
|
|
4
|
-
* domain
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* domain comes up demonstrable: a `/monitors` folder + starter Monitors (each
|
|
7
|
+
* probed once), and a `/status-pages` folder + one StatusPage that `watches` them
|
|
8
|
+
* (its initial status rolled up from the probes). Idempotent: node creates swallow
|
|
9
|
+
* `PATH_CONFLICT` (fixed paths) and re-linking a `watches` edge is a graph no-op.
|
|
10
|
+
* It grants nothing — the installing owner reaches these nodes via root-propagated
|
|
11
|
+
* perms; grant explicitly here for non-owner access.
|
|
8
12
|
*/
|
|
9
13
|
import {
|
|
10
14
|
classify,
|
|
11
|
-
DEPENDS_ON_EDGE,
|
|
12
15
|
FOLDER_CLASS,
|
|
13
16
|
MONITOR_CLASS,
|
|
14
17
|
MONITOR_KEYS,
|
|
15
18
|
MONITORS_PARENT,
|
|
16
19
|
NAME_KEY,
|
|
17
20
|
NODE_CREATE,
|
|
21
|
+
rollup,
|
|
22
|
+
STARTER_PAGE,
|
|
18
23
|
STARTERS,
|
|
24
|
+
STATUS_PAGE_CLASS,
|
|
25
|
+
STATUS_PAGES_PARENT,
|
|
26
|
+
PAGE_KEYS,
|
|
27
|
+
WATCHES_EDGE,
|
|
28
|
+
WATCHES_KEYS,
|
|
19
29
|
} from '../../core/monitor'
|
|
20
|
-
import type
|
|
30
|
+
import { type CallableKernel, isPathConflict } from '../shared'
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export async function seed(kernel: CallableKernel, prober: Prober): Promise<{ seeded: number }> {
|
|
25
|
-
// 1. The `/monitors` folder at the graph root.
|
|
32
|
+
/** Create a node, swallowing a re-seed's `PATH_CONFLICT`. Returns true if newly created. */
|
|
33
|
+
async function ensure(kernel: CallableKernel, params: unknown): Promise<boolean> {
|
|
26
34
|
try {
|
|
27
|
-
await kernel.call(NODE_CREATE,
|
|
28
|
-
|
|
29
|
-
path: MONITORS_PARENT,
|
|
30
|
-
props: { [NAME_KEY]: 'monitors' },
|
|
31
|
-
})
|
|
35
|
+
await kernel.call(NODE_CREATE, params)
|
|
36
|
+
return true
|
|
32
37
|
} catch (e) {
|
|
33
38
|
if (!isPathConflict(e)) throw e
|
|
39
|
+
return false
|
|
34
40
|
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function seed(kernel: CallableKernel, prober: Prober): Promise<{ seeded: number }> {
|
|
44
|
+
await ensure(kernel, {
|
|
45
|
+
class: FOLDER_CLASS,
|
|
46
|
+
path: MONITORS_PARENT,
|
|
47
|
+
props: { [NAME_KEY]: 'monitors' },
|
|
48
|
+
})
|
|
35
49
|
|
|
36
|
-
//
|
|
37
|
-
const
|
|
50
|
+
// Probe + create each starter Monitor; collect the verdict for the page roll-up.
|
|
51
|
+
const members: { status: string; critical: boolean }[] = []
|
|
38
52
|
let seeded = 0
|
|
39
53
|
for (const s of STARTERS) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch (e) {
|
|
57
|
-
if (!isPathConflict(e)) throw e
|
|
58
|
-
}
|
|
54
|
+
const result = await prober.probe(s.url)
|
|
55
|
+
const status = classify(result.statusCode)
|
|
56
|
+
members.push({ status, critical: s.critical })
|
|
57
|
+
const created = await ensure(kernel, {
|
|
58
|
+
class: MONITOR_CLASS,
|
|
59
|
+
path: `${MONITORS_PARENT}/${s.slug}`,
|
|
60
|
+
props: {
|
|
61
|
+
[NAME_KEY]: s.name,
|
|
62
|
+
[MONITOR_KEYS.url]: s.url,
|
|
63
|
+
[MONITOR_KEYS.status]: status,
|
|
64
|
+
[MONITOR_KEYS.statusCode]: result.statusCode,
|
|
65
|
+
[MONITOR_KEYS.latencyMs]: result.latencyMs,
|
|
66
|
+
[MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
if (created) seeded++
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// A StatusPage watching every starter, its status rolled up from the probes.
|
|
73
|
+
await ensure(kernel, {
|
|
74
|
+
class: FOLDER_CLASS,
|
|
75
|
+
path: STATUS_PAGES_PARENT,
|
|
76
|
+
props: { [NAME_KEY]: 'status-pages' },
|
|
77
|
+
})
|
|
78
|
+
const pagePath = `${STATUS_PAGES_PARENT}/${STARTER_PAGE.slug}`
|
|
79
|
+
await ensure(kernel, {
|
|
80
|
+
class: STATUS_PAGE_CLASS,
|
|
81
|
+
path: pagePath,
|
|
82
|
+
props: { [NAME_KEY]: STARTER_PAGE.name, [PAGE_KEYS.status]: rollup(members) },
|
|
83
|
+
})
|
|
84
|
+
// Re-linking the same (page, monitor) `watches` edge is a graph no-op, so this
|
|
85
|
+
// needs no conflict guard.
|
|
86
|
+
for (const s of STARTERS) {
|
|
87
|
+
await kernel.call(`${pagePath}::link`, {
|
|
88
|
+
edgeClass: WATCHES_EDGE,
|
|
89
|
+
target: `${MONITORS_PARENT}/${s.slug}`,
|
|
90
|
+
props: { [WATCHES_KEYS.critical]: s.critical },
|
|
91
|
+
})
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
return { seeded }
|
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import type { CallableKernel } from './shared'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* clock/RNG)
|
|
2
|
+
* `Monitor.watch` (static) — create a Monitor node under `/monitors`. The slug
|
|
3
|
+
* format is pure (`core/monitor`'s `uniqueSlug`); the entropy suffix (impure,
|
|
4
|
+
* clock/RNG) comes from `runtime/shared`, keeping core deterministic.
|
|
7
5
|
*/
|
|
8
6
|
import {
|
|
9
7
|
MONITOR_CLASS,
|
|
10
8
|
MONITOR_KEYS,
|
|
11
9
|
MONITORS_PARENT,
|
|
12
|
-
monitorSlug,
|
|
13
10
|
NAME_KEY,
|
|
14
11
|
NODE_CREATE,
|
|
12
|
+
uniqueSlug,
|
|
15
13
|
} 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
|
-
}
|
|
14
|
+
import { type CallableKernel, randomSuffix } from '../shared'
|
|
21
15
|
|
|
22
16
|
export async function watch(
|
|
23
17
|
kernel: CallableKernel,
|
|
24
18
|
params: { url: string; name?: string },
|
|
25
19
|
): Promise<{ id: string; path: string }> {
|
|
26
|
-
const path = `${MONITORS_PARENT}/${
|
|
20
|
+
const path = `${MONITORS_PARENT}/${uniqueSlug(params.url, randomSuffix())}`
|
|
27
21
|
const created = (await kernel.call(NODE_CREATE, {
|
|
28
22
|
class: MONITOR_CLASS,
|
|
29
23
|
path,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime-internal helpers shared across the
|
|
3
|
-
*
|
|
4
|
-
* operation files agree on.
|
|
2
|
+
* Runtime-internal helpers shared across the context's operations. Not exported
|
|
3
|
+
* from the domain — just the seam the operation files agree on.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -15,3 +14,8 @@ export type CallableKernel = { call(path: string, params: unknown): Promise<unkn
|
|
|
15
14
|
export function isPathConflict(e: unknown): boolean {
|
|
16
15
|
return e instanceof Error && e.message.includes('PATH_CONFLICT')
|
|
17
16
|
}
|
|
17
|
+
|
|
18
|
+
/** Short clock+RNG entropy to dodge same-ms / same-stem slug collisions (impure). */
|
|
19
|
+
export function randomSuffix(): string {
|
|
20
|
+
return `${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CallableKernel } from '../shared'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `StatusPage.add` (instance) — watch a Monitor: create a `watches` edge from
|
|
5
|
+
* this page to `monitor`, tagged `critical` (default false). `monitor` is any
|
|
6
|
+
* node address (a tree path or `@<id>`).
|
|
7
|
+
*/
|
|
8
|
+
import { WATCHES_EDGE, WATCHES_KEYS } from '../../core/monitor'
|
|
9
|
+
|
|
10
|
+
export async function add(
|
|
11
|
+
kernel: CallableKernel,
|
|
12
|
+
selfPathRaw: string,
|
|
13
|
+
params: { monitor: string; critical?: boolean },
|
|
14
|
+
): Promise<{ watched: string }> {
|
|
15
|
+
await kernel.call(`${selfPathRaw}::link`, {
|
|
16
|
+
edgeClass: WATCHES_EDGE,
|
|
17
|
+
target: params.monitor,
|
|
18
|
+
props: { [WATCHES_KEYS.critical]: params.critical ?? false },
|
|
19
|
+
})
|
|
20
|
+
return { watched: params.monitor }
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CallableKernel } from '../shared'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `StatusPage.check` (instance) — implements `Checkable.check` for a page: walk
|
|
5
|
+
* the `watches` edges, re-check each monitor (a cross-node `@<id>::check` call),
|
|
6
|
+
* roll the verdicts up (pure, `core/monitor`), record the page status, return it.
|
|
7
|
+
*
|
|
8
|
+
* `getLinks` returns raw `Edge`s whose endpoint/prop accessors aren't typed yet
|
|
9
|
+
* (an SDK gap) — `parseWatches` is the small cast that bridges it.
|
|
10
|
+
*/
|
|
11
|
+
import { PAGE_KEYS, rollup, WATCHES_EDGE, WATCHES_KEYS } from '../../core/monitor'
|
|
12
|
+
|
|
13
|
+
/** A raw `watches` edge as `getLinks` returns it (endpoints are string-or-`{raw}`). */
|
|
14
|
+
type RawEdge = {
|
|
15
|
+
class?: string | { raw: string }
|
|
16
|
+
target?: string | { raw: string }
|
|
17
|
+
props?: Record<string, unknown>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function rawOf(p: RawEdge['target']): string | undefined {
|
|
21
|
+
if (typeof p === 'string') return p
|
|
22
|
+
return typeof p?.raw === 'string' ? p.raw : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The monitors this page watches: each target ref + its `critical` flag. */
|
|
26
|
+
function parseWatches(raw: unknown): { monitor: string; critical: boolean }[] {
|
|
27
|
+
const edges = (Array.isArray(raw) ? raw : []) as RawEdge[]
|
|
28
|
+
const out: { monitor: string; critical: boolean }[] = []
|
|
29
|
+
for (const e of edges) {
|
|
30
|
+
if (rawOf(e.class) !== WATCHES_EDGE) continue
|
|
31
|
+
const monitor = rawOf(e.target)
|
|
32
|
+
if (monitor) out.push({ monitor, critical: e.props?.[WATCHES_KEYS.critical] === true })
|
|
33
|
+
}
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function check(
|
|
38
|
+
kernel: CallableKernel,
|
|
39
|
+
selfPathRaw: string,
|
|
40
|
+
): Promise<{ status: string }> {
|
|
41
|
+
const links = await kernel.call(`${selfPathRaw}::getLinks`, { direction: 'out' })
|
|
42
|
+
const members = []
|
|
43
|
+
for (const w of parseWatches(links)) {
|
|
44
|
+
const { status } = (await kernel.call(`${w.monitor}::check`, {})) as { status: string }
|
|
45
|
+
members.push({ status, critical: w.critical })
|
|
46
|
+
}
|
|
47
|
+
const status = rollup(members)
|
|
48
|
+
await kernel.call(`${selfPathRaw}::update`, { props: { [PAGE_KEYS.status]: status } })
|
|
49
|
+
return { status }
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StatusPage.create` (static) — create a StatusPage node under `/status-pages`.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
NAME_KEY,
|
|
6
|
+
NODE_CREATE,
|
|
7
|
+
STATUS_PAGE_CLASS,
|
|
8
|
+
STATUS_PAGES_PARENT,
|
|
9
|
+
uniqueSlug,
|
|
10
|
+
} from '../../core/monitor'
|
|
11
|
+
import { type CallableKernel, randomSuffix } from '../shared'
|
|
12
|
+
|
|
13
|
+
export async function create(
|
|
14
|
+
kernel: CallableKernel,
|
|
15
|
+
params: { name: string },
|
|
16
|
+
): Promise<{ id: string; path: string }> {
|
|
17
|
+
const path = `${STATUS_PAGES_PARENT}/${uniqueSlug(params.name, randomSuffix())}`
|
|
18
|
+
const created = (await kernel.call(NODE_CREATE, {
|
|
19
|
+
class: STATUS_PAGE_CLASS,
|
|
20
|
+
path,
|
|
21
|
+
props: { [NAME_KEY]: params.name },
|
|
22
|
+
})) as { id: string }
|
|
23
|
+
return { id: created.id, path }
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusPage operations — one file per method, assembled here for the
|
|
3
|
+
* composition root (`runtime/index.ts`). Transport-agnostic logic over a
|
|
4
|
+
* `CallableKernel`, delegating the roll-up decision to `core/monitor`.
|
|
5
|
+
*/
|
|
6
|
+
export { add } from './add'
|
|
7
|
+
export { check } from './check'
|
|
8
|
+
export { create } from './create'
|
package/template/schema/index.ts
CHANGED
|
@@ -11,19 +11,19 @@
|
|
|
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 { Checkable, Monitor, StatusPage, watches } from './monitor'
|
|
15
15
|
|
|
16
16
|
export const schema = defineSchema('astrale-domain.example.dev', {
|
|
17
|
-
interfaces: {
|
|
18
|
-
classes: { Monitor,
|
|
17
|
+
interfaces: { Checkable },
|
|
18
|
+
classes: { Monitor, StatusPage, watches },
|
|
19
19
|
imports: [KernelSchema],
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The compiled domain — resolved class/interface paths + qualified prop keys.
|
|
24
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
|
|
26
|
-
* of graph-facing strings (class paths, prop keys) — never hand-write those.
|
|
25
|
+
* and from build tooling alike. `core/monitor/keys.ts` re-exports it as the single
|
|
26
|
+
* source of graph-facing strings (class paths, prop keys) — never hand-write those.
|
|
27
27
|
*/
|
|
28
28
|
export const D: Domain<typeof schema> = compileDomain(schema)
|
|
29
29
|
|