@astrale-os/adapter-cloudflare 0.1.10 → 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.
Files changed (65) hide show
  1. package/package.json +2 -2
  2. package/template/.agents/skills/astrale-cli/SKILL.md +1 -1
  3. package/template/.agents/skills/astrale-domain/SKILL.md +2 -1
  4. package/template/client/README.md +25 -23
  5. package/template/client/__tests__/app.test.tsx +44 -88
  6. package/template/client/__tests__/harness.ts +11 -16
  7. package/template/client/__tests__/kernel.test.ts +9 -35
  8. package/template/client/__tests__/seam.test.tsx +22 -18
  9. package/template/client/src/app.tsx +11 -17
  10. package/template/client/src/shell/use-capability.ts +1 -3
  11. package/template/client/src/shell/use-node.ts +2 -2
  12. package/template/client/src/shell/view-router.tsx +3 -4
  13. package/template/client/src/status/components/StatusCard.tsx +50 -0
  14. package/template/client/src/status/components/index.ts +1 -0
  15. package/template/client/src/status/hooks/index.ts +3 -0
  16. package/template/client/src/{monitor → status}/hooks/useCheck.mutation.ts +2 -2
  17. package/template/client/src/{monitor/hooks/useMonitor.query.ts → status/hooks/useCheckable.query.ts} +8 -8
  18. package/template/client/src/status/index.ts +7 -0
  19. package/template/client/src/status/status.api.ts +12 -0
  20. package/template/client/src/status/status.mappers.ts +19 -0
  21. package/template/client/src/status/status.types.ts +11 -0
  22. package/template/client/src/styles.css +5 -0
  23. package/template/client/src/ui/StatusBadge.tsx +31 -0
  24. package/template/client/src/ui/index.ts +6 -2
  25. package/template/client/src/views/status.tsx +28 -0
  26. package/template/client/vite.config.ts +2 -3
  27. package/template/client/vitest.config.ts +1 -2
  28. package/template/core/monitor/health.ts +19 -4
  29. package/template/core/monitor/keys.ts +14 -2
  30. package/template/core/monitor/node.ts +27 -21
  31. package/template/deps.ts +2 -1
  32. package/template/integrations/prober/http.ts +4 -15
  33. package/template/integrations/prober/mock.ts +1 -5
  34. package/template/integrations/prober/port.ts +0 -2
  35. package/template/integrations/prober/registry.ts +6 -7
  36. package/template/package.json +2 -2
  37. package/template/pnpm-workspace.yaml +2 -0
  38. package/template/runtime/index.ts +51 -39
  39. package/template/runtime/monitor/check.ts +9 -9
  40. package/template/runtime/monitor/index.ts +4 -7
  41. package/template/runtime/monitor/seed.ts +67 -46
  42. package/template/runtime/monitor/watch.ts +6 -12
  43. package/template/runtime/{monitor/shared.ts → shared.ts} +7 -3
  44. package/template/runtime/status-page/add.ts +21 -0
  45. package/template/runtime/status-page/check.ts +50 -0
  46. package/template/runtime/status-page/create.ts +24 -0
  47. package/template/runtime/status-page/index.ts +8 -0
  48. package/template/schema/index.ts +5 -5
  49. package/template/schema/monitor.ts +62 -48
  50. package/template/views/index.ts +4 -5
  51. package/template/views/status-page.ts +16 -0
  52. package/template/client/src/monitor/components/MonitorCard.tsx +0 -50
  53. package/template/client/src/monitor/components/index.ts +0 -1
  54. package/template/client/src/monitor/hooks/index.ts +0 -3
  55. package/template/client/src/monitor/index.ts +0 -6
  56. package/template/client/src/monitor/monitor.api.ts +0 -11
  57. package/template/client/src/monitor/monitor.mappers.ts +0 -38
  58. package/template/client/src/monitor/monitor.types.ts +0 -23
  59. package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +0 -38
  60. package/template/client/src/monitor/ui/StatusBadge.UI.tsx +0 -14
  61. package/template/client/src/monitor/ui/index.ts +0 -8
  62. package/template/client/src/views/monitor.tsx +0 -30
  63. package/template/pnpm-lock.yaml +0 -2766
  64. package/template/runtime/monitor/dependsOn.ts +0 -16
  65. package/template/views/monitor.ts +0 -22
@@ -1,57 +1,43 @@
1
1
  /**
2
- * Monitor context — the domain's single bounded slice.
2
+ * Monitoring context — the domain's single bounded slice: a `Checkable` contract
3
+ * implemented by two classes, plus the edge that binds them.
3
4
  *
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`).
5
+ * - Interface `Checkable` one `abstract` method, `check()`. Abstract each
6
+ * implementing class brings its OWN body, and the
7
+ * kernel dispatches by the node's class.
8
+ * - Class `Monitor` a single HTTP check. `check()` probes `url`;
9
+ * `watch` creates one; `seed` lays down the demo set.
10
+ * - Class `StatusPage` a roll-up. `check()` re-checks the monitors it
11
+ * `watches` and aggregates; `create` makes a page;
12
+ * `add` watches a monitor.
13
+ * - Edge `watches` StatusPage → Monitor, with a `critical` flag.
18
14
  */
19
- import { edgeClass, KernelSchema, nodeClass, nodeInterface } from '@astrale-os/kernel-core'
15
+ import { edgeClass, nodeClass, nodeInterface } from '@astrale-os/kernel-core'
20
16
  import { fn } from '@astrale-os/kernel-dsl'
21
17
  import { z } from 'zod'
22
18
 
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() })
19
+ /** Thin ref to a created node — what the static factories return (a full Node
20
+ * value does not round-trip over the worker wire). */
21
+ export const NodeRef = z.object({ id: z.string(), path: z.string() })
29
22
 
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
- })
23
+ /** What `check()` reports the subject's current health verdict. */
24
+ export const CheckResult = z.object({ status: z.string() })
36
25
 
37
- export const MonitorOps = nodeInterface({
26
+ export const Checkable = nodeInterface({
38
27
  methods: {
39
- watch: fn({
40
- static: true,
41
- params: { url: z.string(), name: z.string().optional() },
42
- returns: MonitorRef,
43
- }),
28
+ // `abstract` → no shared body; every implementer supplies its own `check`,
29
+ // wired per-class in `runtime/`. `@<node>::check` dispatches by the node's class.
30
+ check: fn({ inheritance: 'abstract', returns: CheckResult }),
44
31
  },
45
32
  })
46
33
 
47
34
  export const Monitor = nodeClass({
48
- implements: [MonitorOps, KernelSchema.interfaces.Container],
35
+ implements: [Checkable],
49
36
  props: {
50
37
  /** The target URL this monitor probes. */
51
38
  url: z.string(),
52
39
  // 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).
40
+ // silently drops `z.enum()` props. Values: 'up' | 'down' | 'unknown'.
55
41
  status: z.string().optional(),
56
42
  /** Last observed HTTP status code (0 = host unreachable). */
57
43
  statusCode: z.number().int().optional(),
@@ -61,20 +47,48 @@ export const Monitor = nodeClass({
61
47
  lastCheckedAt: z.string().optional(),
62
48
  },
63
49
  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() }) }),
50
+ // Implements `Checkable.check` for a single endpoint (probe `url`). Redeclared
51
+ // here a concrete class must declare each abstract method it implements, so
52
+ // the compiler materializes Monitor's OWN `check` node for per-class dispatch.
53
+ // The body lives in `runtime/monitor/check.ts`.
54
+ check: fn({ returns: CheckResult }),
55
+ /** Create a Monitor under `/monitors`. */
56
+ watch: fn({
57
+ static: true,
58
+ params: { url: z.string(), name: z.string().optional() },
59
+ returns: NodeRef,
60
+ }),
69
61
  // 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.
62
+ // kernel calls it ONCE after install, as __SYSTEM__. Must stay idempotent.
72
63
  seed: fn({ static: true, returns: z.object({ seeded: z.number().int() }) }),
73
64
  },
74
65
  })
75
66
 
76
- export const depends_on = edgeClass(
77
- { as: 'dependent', types: [Monitor] },
78
- { as: 'dependency', types: [Monitor] },
79
- { props: { reason: z.string().optional() } },
67
+ export const StatusPage = nodeClass({
68
+ implements: [Checkable],
69
+ props: {
70
+ /** Rolled-up status, written by `check`. 'up' | 'degraded' | 'down' | 'unknown'. */
71
+ status: z.string().optional(),
72
+ },
73
+ methods: {
74
+ // Implements `Checkable.check` for a page (roll up the watched monitors).
75
+ // Redeclared so StatusPage materializes its OWN `check` node. Body in
76
+ // `runtime/status-page/check.ts`.
77
+ check: fn({ returns: CheckResult }),
78
+ /** Create a StatusPage under `/status-pages`. */
79
+ create: fn({ static: true, params: { name: z.string() }, returns: NodeRef }),
80
+ /** Watch a monitor (a `watches` edge); `critical` decides the roll-up weight. */
81
+ add: fn({
82
+ params: { monitor: z.string(), critical: z.boolean().optional() },
83
+ returns: z.object({ watched: z.string() }),
84
+ }),
85
+ },
86
+ })
87
+
88
+ export const watches = edgeClass(
89
+ { as: 'page', types: [StatusPage] },
90
+ { as: 'monitor', types: [Monitor] },
91
+ // `critical` drives the roll-up: a critical monitor down ⇒ page `down`; a
92
+ // non-critical one down ⇒ `degraded`.
93
+ { props: { critical: z.boolean() } },
80
94
  )
@@ -4,14 +4,13 @@
4
4
  * binding the SDK stamps with the worker's live serving URL when it builds the
5
5
  * install bundle.
6
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.
7
+ * `welcome` is a worker-rendered inline-HTML view (no SPA). `ui-status-page` is
8
+ * the `client/` SPA (mounted at `/ui/status-page`, offered for any StatusPage).
10
9
  */
11
- import { monitor } from './monitor'
10
+ import { statusPage } from './status-page'
12
11
  import { welcome } from './welcome'
13
12
 
14
13
  export const views = {
15
14
  welcome,
16
- 'ui-monitor': monitor,
15
+ 'ui-status-page': statusPage,
17
16
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `ui-status-page` — the domain's SPA view, mounted at `/ui/status-page` and
3
+ * served from the `client/` bundle. `viewFor: selfOf(StatusPage)` attaches a
4
+ * `view_for` edge to the `StatusPage` class meta-node, so the GUI offers this
5
+ * view for any StatusPage instance.
6
+ */
7
+ import { selfOf } from '@astrale-os/kernel-dsl'
8
+ import { defineView } from '@astrale-os/sdk'
9
+
10
+ import { StatusPage } from '../schema/monitor'
11
+
12
+ export const statusPage = defineView({
13
+ auth: 'public',
14
+ mount: '/ui/status-page',
15
+ viewFor: selfOf(StatusPage),
16
+ })
@@ -1,50 +0,0 @@
1
- /**
2
- * MonitorCard — the Monitor feature's one container. It owns the data/logic:
3
- * loads the node (`useMonitor`), wires the `check` write (`useCheck`), and
4
- * composes the presentation. Handles the query's loading/error/ok states; on
5
- * `ok` it renders a `Panel` whose body is the check-error banner (if any) plus
6
- * the pure `MonitorDetails` view, with a "Check now" button that runs the probe
7
- * then reloads the node so the fresh values render.
8
- *
9
- * Thin by design: the record's layout lives in `monitor/ui` (`MonitorDetails`);
10
- * the generic surfaces come from the `@/ui` design system.
11
- */
12
- import type { KernelClient } from '@/shell'
13
-
14
- import { ErrorBanner, Panel, Spinner } from '@/ui'
15
-
16
- import { useCheck, useMonitor } from '../hooks'
17
- import { MonitorDetails } from '../ui'
18
-
19
- export function MonitorCard({ session, nodeId }: { session: KernelClient; nodeId: string }) {
20
- const monitor = useMonitor(session, nodeId)
21
- const probe = useCheck(session, nodeId)
22
-
23
- async function checkNow() {
24
- if (await probe.run()) monitor.reload()
25
- }
26
-
27
- if (monitor.state === 'idle' || monitor.state === 'loading') {
28
- return <Spinner label="Loading the Monitor…" />
29
- }
30
- if (monitor.state === 'error') {
31
- return <ErrorBanner>Failed to load the Monitor: {monitor.message}</ErrorBanner>
32
- }
33
-
34
- const record = monitor.record!
35
- const checking = probe.phase === 'running'
36
- const checkButton = (
37
- <button type="button" className="check-btn" onClick={checkNow} disabled={checking}>
38
- {checking ? 'Checking…' : 'Check now'}
39
- </button>
40
- )
41
-
42
- return (
43
- <Panel title={record.name} actions={checkButton}>
44
- {probe.phase === 'failed' && probe.error && (
45
- <ErrorBanner>Check failed: {probe.error}</ErrorBanner>
46
- )}
47
- <MonitorDetails record={record} />
48
- </Panel>
49
- )
50
- }
@@ -1 +0,0 @@
1
- export { MonitorCard } from './MonitorCard'
@@ -1,3 +0,0 @@
1
- export { useMonitor } from './useMonitor.query'
2
- export type { MonitorQuery } from './useMonitor.query'
3
- export { useCheck } from './useCheck.mutation'
@@ -1,6 +0,0 @@
1
- /** The Monitor feature — its api, types, mappers, hooks and components. */
2
- export * from './components'
3
- export * from './hooks'
4
- export { check } from './monitor.api'
5
- export { monitorFromNode, normalizeStatus } from './monitor.mappers'
6
- export type { MonitorRecord } from './monitor.types'
@@ -1,11 +0,0 @@
1
- /** Raw kernel calls for the Monitor feature — node instance methods. */
2
- import { invokeNode, type KernelClient } from '@/shell'
3
-
4
- /**
5
- * Run the Monitor's `check` instance method (`@<id>::check`). Probes the target
6
- * URL server-side and updates the node's `status`/`statusCode`/`latencyMs`/
7
- * `lastCheckedAt` props; the caller reloads the node to render the fresh values.
8
- */
9
- export function check(session: KernelClient, nodeId: string): Promise<unknown> {
10
- return invokeNode(session, nodeId, 'check', {})
11
- }
@@ -1,38 +0,0 @@
1
- /** Node → typed record transforms for the Monitor feature. */
2
- import { type KernelNode, PROP, readProp, readPropBySuffix } from '@/shell'
3
-
4
- import type { MonitorRecord, MonitorStatus } from './monitor.types'
5
-
6
- const lastSegment = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
7
-
8
- /** Coerce a string prop to a finite number, or `undefined`. */
9
- function readNumberBySuffix(props: Record<string, unknown>, suffix: string): number | undefined {
10
- const raw = readPropBySuffix(props, suffix)
11
- if (raw === undefined) return undefined
12
- const n = Number(raw)
13
- return Number.isFinite(n) ? n : undefined
14
- }
15
-
16
- /** Normalize the node's `status` prop to the three known states. */
17
- export function normalizeStatus(raw: string | undefined): MonitorStatus {
18
- return raw === 'up' || raw === 'down' ? raw : 'unknown'
19
- }
20
-
21
- /**
22
- * Project a Monitor `KernelNode` into a `MonitorRecord`. The name comes from the
23
- * kernel `Named.name` key; the domain props (`url`/`status`/`statusCode`/
24
- * `latencyMs`/`lastCheckedAt`) are domain-qualified, so read them by suffix.
25
- */
26
- export function monitorFromNode(node: KernelNode): MonitorRecord {
27
- const p = node.props ?? {}
28
- return {
29
- id: node.id,
30
- path: node.path ?? '',
31
- name: readProp(p, PROP.named.name) ?? lastSegment(node.path ?? '') ?? node.id,
32
- url: readPropBySuffix(p, '.property.url') ?? '',
33
- status: normalizeStatus(readPropBySuffix(p, '.property.status')),
34
- statusCode: readNumberBySuffix(p, '.property.statusCode'),
35
- latencyMs: readNumberBySuffix(p, '.property.latencyMs'),
36
- lastCheckedAt: readPropBySuffix(p, '.property.lastCheckedAt'),
37
- }
38
- }
@@ -1,23 +0,0 @@
1
- /** The Monitor feature's client types. */
2
-
3
- /** The three known states of a Monitor's normalized status. */
4
- export type MonitorStatus = 'up' | 'down' | 'unknown'
5
-
6
- /**
7
- * Client-side projection of a Monitor node. Domain props are read off the
8
- * kernel node by suffix (see `monitor.mappers.ts`); numeric props are coerced.
9
- */
10
- export type MonitorRecord = {
11
- id: string
12
- path: string
13
- name: string
14
- url: string
15
- /** up | down | unknown — normalized from the node's `status` prop. */
16
- status: MonitorStatus
17
- /** HTTP status code from the last check, if any. */
18
- statusCode?: number
19
- /** Round-trip latency of the last check, in milliseconds. */
20
- latencyMs?: number
21
- /** ISO timestamp of the last check, if any. */
22
- lastCheckedAt?: string
23
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * MonitorDetails — the Monitor record's PRESENTATION, extracted pure. Given a
3
- * projected `MonitorRecord`, it lays out the StatusBadge + the url/latency/
4
- * statusCode/last-checked KV rows using the feature-agnostic `@/ui` primitives.
5
- *
6
- * Pure: no hooks, no kernel, no data loading — props in, DOM out. The container
7
- * (`MonitorCard`) owns loading the record and wiring the "Check now" write.
8
- */
9
- import { ExternalLink, KV, Mono, relativeTime } from '@/ui'
10
-
11
- import type { MonitorRecord } from '../monitor.types'
12
-
13
- import { StatusBadge } from './StatusBadge.UI'
14
-
15
- export function MonitorDetails({ record }: { record: MonitorRecord }) {
16
- return (
17
- <>
18
- <div className="status-row">
19
- <StatusBadge status={record.status} />
20
- </div>
21
-
22
- <div className="kv">
23
- <KV label="url">
24
- <ExternalLink url={record.url || undefined} />
25
- </KV>
26
- <KV label="latency">
27
- <Mono value={record.latencyMs !== undefined ? `${record.latencyMs}ms` : undefined} />
28
- </KV>
29
- <KV label="status code">
30
- <Mono value={record.statusCode !== undefined ? String(record.statusCode) : undefined} />
31
- </KV>
32
- <KV label="last checked">
33
- <Mono value={relativeTime(record.lastCheckedAt)} title={record.lastCheckedAt} />
34
- </KV>
35
- </div>
36
- </>
37
- )
38
- }
@@ -1,14 +0,0 @@
1
- /** Monitor status chip — pure presentational; styling lives in `styles.css`. */
2
-
3
- import type { MonitorStatus } from '../monitor.types'
4
-
5
- /**
6
- * Color-coded status pill: up = green, down = red, unknown = muted. The label is
7
- * the uppercased status (`UP` / `DOWN` / `UNKNOWN`). Monitor-specific — it knows
8
- * the feature's up/down/unknown vocabulary — so it lives in `monitor/ui`, not the
9
- * feature-agnostic `@/ui` design system.
10
- */
11
- export function StatusBadge({ status }: { status: MonitorStatus }) {
12
- const label = status === 'up' ? 'UP' : status === 'down' ? 'DOWN' : 'UNKNOWN'
13
- return <span className={`status-badge status-${status}`}>{label}</span>
14
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * The Monitor feature's OWN presentation — monitor-specific, feature-aware UI
3
- * (it knows the up/down/unknown status vocabulary and the record shape). Pure
4
- * components, built on the feature-agnostic `@/ui` primitives. Import within the
5
- * feature: `import { StatusBadge, MonitorDetails } from '../ui'`.
6
- */
7
- export { StatusBadge } from './StatusBadge.UI'
8
- export { MonitorDetails } from './MonitorDetails.UI'
@@ -1,30 +0,0 @@
1
- /**
2
- * The `ui-monitor` view (mount path `/ui/monitor`) — the Monitor detail panel.
3
- *
4
- * Mounted by the Astrale shell as a sandboxed iframe; `@/shell` (built on the
5
- * real `@astrale-os/shell`) completes the handshake and hands over the kernel
6
- * session + the target node id. `ViewFrame` gates the handshake (loading /
7
- * standalone / ready); the `ready` body delegates to `MonitorCard`, the feature
8
- * container that loads the node, renders its status/url/latency, and exposes a
9
- * "Check now" probe.
10
- *
11
- * Pure composition — no data/transport logic lives here (that's `@/monitor` +
12
- * `@/shell`).
13
- */
14
- import { MonitorCard } from '@/monitor'
15
- import { type ShellState, ViewFrame } from '@/shell'
16
-
17
- export function MonitorView(shell: ShellState) {
18
- return (
19
- <ViewFrame shell={shell} title="Status monitor" subline="ui-monitor · Astrale view SPA">
20
- {(session, nodeId) =>
21
- nodeId ? (
22
- // Keyed by node id so a target hot-swap remounts with fresh state.
23
- <MonitorCard key={nodeId} session={session} nodeId={nodeId} />
24
- ) : (
25
- <div className="banner">No target Monitor — open this view from a Monitor node.</div>
26
- )
27
- }
28
- </ViewFrame>
29
- )
30
- }