@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,7 +1,7 @@
1
- import { useCallback, useEffect, useState } from 'react'
2
-
3
1
  import type { KernelClient } from '@astrale-os/shell'
4
2
 
3
+ import { useCallback, useEffect, useState } from 'react'
4
+
5
5
  import { errorMessage, type KernelNode } from './client'
6
6
 
7
7
  export type NodeState =
@@ -2,7 +2,7 @@
2
2
  * Tiny multi-view router for the domain's one SPA bundle.
3
3
  *
4
4
  * The domain declares several SPA Views, each mounted by the shell at its own
5
- * path under `/ui/*` (e.g. `/ui/monitor`). The shell mounts ONE iframe per view,
5
+ * path under `/ui/*` (e.g. `/ui/status-page`). The shell mounts ONE iframe per view,
6
6
  * so inside the iframe `window.location.pathname` is that view's mount path. This
7
7
  * bundle reads that path and renders the matching view — one build, many views.
8
8
  *
@@ -16,16 +16,15 @@
16
16
  * matching `defineView({ mount })` in the domain's `views/`.
17
17
  */
18
18
 
19
- import type { ReactNode } from 'react'
20
-
21
19
  import type { KernelClient } from '@astrale-os/shell'
20
+ import type { ReactNode } from 'react'
22
21
 
23
22
  import type { ShellState } from './use-shell'
24
23
 
25
24
  /** A view: given the shell state, render its tree. */
26
25
  export type ViewComponent = (shell: ShellState) => ReactNode
27
26
 
28
- /** Mount path → view component (key e.g. `/ui/monitor`). */
27
+ /** Mount path → view component (key e.g. `/ui/status-page`). */
29
28
  export type ViewRoutes = Record<string, ViewComponent>
30
29
 
31
30
  /** Strip a single trailing slash (but keep a bare `/`). */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * StatusCard — the status feature's one container. It owns the data/logic: loads
3
+ * the node (`useCheckable`), wires the `check` write (`useCheck`), and composes
4
+ * the presentation. Handles the query's loading/error/ok states; on `ok` it
5
+ * renders a `Panel` with the check-error banner (if any) + the StatusBadge.
6
+ * "Check now" runs the check then reloads the node so the fresh status renders.
7
+ *
8
+ * The generic surfaces come from the `@/ui` design system; the only domain-shaped
9
+ * primitive is the shared `StatusBadge`.
10
+ */
11
+ import type { KernelClient } from '@/shell'
12
+
13
+ import { ErrorBanner, Panel, Spinner, StatusBadge } from '@/ui'
14
+
15
+ import { useCheck, useCheckable } from '../hooks'
16
+
17
+ export function StatusCard({ session, nodeId }: { session: KernelClient; nodeId: string }) {
18
+ const checkable = useCheckable(session, nodeId)
19
+ const check = useCheck(session, nodeId)
20
+
21
+ async function checkNow() {
22
+ if (await check.run()) checkable.reload()
23
+ }
24
+
25
+ if (checkable.state === 'idle' || checkable.state === 'loading') {
26
+ return <Spinner label="Loading…" />
27
+ }
28
+ if (checkable.state === 'error') {
29
+ return <ErrorBanner>Failed to load: {checkable.message}</ErrorBanner>
30
+ }
31
+
32
+ const record = checkable.record!
33
+ const checking = check.phase === 'running'
34
+ const checkButton = (
35
+ <button type="button" className="check-btn" onClick={checkNow} disabled={checking}>
36
+ {checking ? 'Checking…' : 'Check now'}
37
+ </button>
38
+ )
39
+
40
+ return (
41
+ <Panel title={record.name} actions={checkButton}>
42
+ {check.phase === 'failed' && check.error && (
43
+ <ErrorBanner>Check failed: {check.error}</ErrorBanner>
44
+ )}
45
+ <div className="status-row">
46
+ <StatusBadge status={record.status} />
47
+ </div>
48
+ </Panel>
49
+ )
50
+ }
@@ -0,0 +1 @@
1
+ export { StatusCard } from './StatusCard'
@@ -0,0 +1,3 @@
1
+ export { useCheckable } from './useCheckable.query'
2
+ export type { CheckableQuery } from './useCheckable.query'
3
+ export { useCheck } from './useCheck.mutation'
@@ -1,7 +1,7 @@
1
- /** Run a Monitor's `check` probe — the feature's one write capability. */
1
+ /** Run a checkable node's `check` — the feature's one write capability. */
2
2
  import { type Capability, type KernelClient, useCapability } from '@/shell'
3
3
 
4
- import { check } from '../monitor.api'
4
+ import { check } from '../status.api'
5
5
 
6
6
  /**
7
7
  * Wrap `@<id>::check` in the shared `useCapability` lifecycle (idle → running →
@@ -1,17 +1,17 @@
1
- /** Load a Monitor node → typed record, with a `reload()` for post-check refresh. */
1
+ /** Load a checkable node → typed record, with a `reload()` for post-check refresh. */
2
2
  import { useCallback, useRef, useState } from 'react'
3
3
 
4
4
  import { type KernelClient, useNode } from '@/shell'
5
5
 
6
- import type { MonitorRecord } from '../monitor.types'
6
+ import type { CheckableRecord } from '../status.types'
7
7
 
8
- import { monitorFromNode } from '../monitor.mappers'
8
+ import { checkableFromNode } from '../status.mappers'
9
9
 
10
- export type MonitorQuery = {
10
+ export type CheckableQuery = {
11
11
  /** Lifecycle of the underlying `@<id>::get`. */
12
12
  state: 'idle' | 'loading' | 'error' | 'ok'
13
13
  /** The projected record once `ok`. */
14
- record?: MonitorRecord
14
+ record?: CheckableRecord
15
15
  /** Failure message once `error`. */
16
16
  message?: string
17
17
  /** Re-fetch the node — call after `check` mutates it server-side. */
@@ -21,13 +21,13 @@ export type MonitorQuery = {
21
21
  }
22
22
 
23
23
  /**
24
- * Load the Monitor node `nodeId` and project it to a `MonitorRecord`. Wraps
24
+ * Load the checkable node `nodeId` and project it to a `CheckableRecord`. Wraps
25
25
  * `useNode` (which owns the `@<id>::get` + the hot-swap re-fetch) and re-exposes
26
26
  * its `reload()` so the view can refresh after a `check`. `reloading` is true
27
27
  * from the moment `reload()` is called until the re-fetch settles — distinct from
28
28
  * the initial `loading`, so the view can show the existing record meanwhile.
29
29
  */
30
- export function useMonitor(session: KernelClient, nodeId: string): MonitorQuery {
30
+ export function useCheckable(session: KernelClient, nodeId: string): CheckableQuery {
31
31
  const node = useNode(session, nodeId)
32
32
  const [reloading, setReloading] = useState(false)
33
33
  // Two-phase reload tracker. `reload()` arms it as `'requested'`; once we
@@ -53,7 +53,7 @@ export function useMonitor(session: KernelClient, nodeId: string): MonitorQuery
53
53
 
54
54
  switch (node.status) {
55
55
  case 'ok':
56
- return { state: 'ok', record: monitorFromNode(node.node), reload, reloading }
56
+ return { state: 'ok', record: checkableFromNode(node.node), reload, reloading }
57
57
  case 'error':
58
58
  return { state: 'error', message: node.message, reload, reloading }
59
59
  case 'loading':
@@ -0,0 +1,7 @@
1
+ /** The status feature — its api, types, mappers, hooks and components. One card,
2
+ * polymorphic over any `Checkable` node (Monitor or StatusPage). */
3
+ export * from './components'
4
+ export * from './hooks'
5
+ export { check } from './status.api'
6
+ export { checkableFromNode } from './status.mappers'
7
+ export type { CheckableRecord } from './status.types'
@@ -0,0 +1,12 @@
1
+ /** Raw kernel calls for the status feature — node instance methods. */
2
+ import { invokeNode, type KernelClient } from '@/shell'
3
+
4
+ /**
5
+ * Run a checkable node's `check` instance method (`@<id>::check`). The single
6
+ * write both classes share: a Monitor probes its target URL, a StatusPage rolls
7
+ * its watched monitors up — each updates its own `status` prop server-side; the
8
+ * caller reloads the node to render the fresh value.
9
+ */
10
+ export function check(session: KernelClient, nodeId: string): Promise<unknown> {
11
+ return invokeNode(session, nodeId, 'check', {})
12
+ }
@@ -0,0 +1,19 @@
1
+ /** Node → typed record transform for the status feature. */
2
+ import { type KernelNode, PROP, readProp, readPropBySuffix } from '@/shell'
3
+
4
+ import type { CheckableRecord } from './status.types'
5
+
6
+ const lastSegment = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
7
+
8
+ /**
9
+ * Project a `Checkable` `KernelNode` into a `CheckableRecord`: the name from the
10
+ * kernel `Named.name` key, the domain-qualified `status` read by suffix (default
11
+ * `'unknown'`).
12
+ */
13
+ export function checkableFromNode(node: KernelNode): CheckableRecord {
14
+ const p = node.props ?? {}
15
+ return {
16
+ name: readProp(p, PROP.named.name) ?? lastSegment(node.path ?? '') ?? node.id,
17
+ status: readPropBySuffix(p, '.property.status') ?? 'unknown',
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ /** The status feature's client type — what a `Checkable` node projects to. */
2
+
3
+ /**
4
+ * Client-side projection of the `Checkable` node the view renders (a StatusPage).
5
+ * `status` stays a plain string — the page's rolled-up verdict
6
+ * (up/degraded/down/unknown), which `StatusBadge` renders.
7
+ */
8
+ export type CheckableRecord = {
9
+ name: string
10
+ status: string
11
+ }
@@ -13,6 +13,7 @@
13
13
  --err: #dc2626;
14
14
  --err-soft: rgba(220, 38, 38, 0.1);
15
15
  --up: #16a34a;
16
+ --degraded: #d97706;
16
17
  --down: #dc2626;
17
18
  --unknown: #737373;
18
19
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -198,6 +199,10 @@ body {
198
199
  background: var(--up);
199
200
  }
200
201
 
202
+ .status-degraded {
203
+ background: var(--degraded);
204
+ }
205
+
201
206
  .status-down {
202
207
  background: var(--down);
203
208
  }
@@ -0,0 +1,31 @@
1
+ /** Health status chip — pure presentational; styling lives in `styles.css`. */
2
+
3
+ /** The health vocabulary the badge styles, shared across checkable nodes. */
4
+ export type HealthStatus = 'up' | 'down' | 'degraded' | 'unknown'
5
+
6
+ /** Map any raw status string to the four styled states (unrecognized → unknown). */
7
+ function normalize(status: string): HealthStatus {
8
+ return status === 'up' || status === 'down' || status === 'degraded' ? status : 'unknown'
9
+ }
10
+
11
+ /**
12
+ * Color-coded status pill: up = green, degraded = amber, down = red, unknown =
13
+ * muted. The label is the uppercased status (`UP` / `DEGRADED` / `DOWN` /
14
+ * `UNKNOWN`). Takes any raw status string — the vocabulary depends on the node
15
+ * type (up/down for a Monitor, up/degraded/down for a StatusPage) — and styles
16
+ * the four known states, falling back to `unknown` for anything else. Both
17
+ * checkable node types render through it, so it lives in the feature-agnostic
18
+ * `@/ui`, not a feature's own folder.
19
+ */
20
+ export function StatusBadge({ status }: { status: string }) {
21
+ const known = normalize(status)
22
+ const label =
23
+ known === 'up'
24
+ ? 'UP'
25
+ : known === 'degraded'
26
+ ? 'DEGRADED'
27
+ : known === 'down'
28
+ ? 'DOWN'
29
+ : 'UNKNOWN'
30
+ return <span className={`status-badge status-${known}`}>{label}</span>
31
+ }
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * The design system — generic, feature-AGNOSTIC presentational primitives +
3
- * display formatters, no domain knowledge and no kernel session. Feature-specific
4
- * UI (e.g. the Monitor's StatusBadge) lives in that feature's own `ui/`, not here.
3
+ * display formatters, no domain knowledge and no kernel session. The one
4
+ * domain-shaped exception is `StatusBadge`: it renders the up/degraded/down/
5
+ * unknown health vocabulary that any `Checkable` node reports, so it's a shared
6
+ * primitive here rather than living inside the status feature.
5
7
  * Import from the barrel: `import { Panel, KV, relativeTime } from '@/ui'`.
6
8
  */
7
9
  export { EmptyState, ErrorBanner, Panel, Spinner } from './surface'
10
+ export { StatusBadge } from './StatusBadge'
11
+ export type { HealthStatus } from './StatusBadge'
8
12
  export { ExternalLink, KV, Mono } from './value'
9
13
  export { relativeTime } from './format'
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The status view — the SPA's one view: the StatusPage panel at `/ui/status-page`.
3
+ * Mounted by the Astrale shell as a sandboxed iframe; `@/shell` (built on the real
4
+ * `@astrale-os/shell`) completes the handshake and hands over the kernel session +
5
+ * the target node id. `ViewFrame` gates the handshake (loading / standalone /
6
+ * ready); the `ready` body delegates to `StatusCard`, the feature container that
7
+ * loads the node, renders its rolled-up status, and exposes a "Check now" action.
8
+ *
9
+ * Pure composition — no data/transport logic lives here (that's `@/status` +
10
+ * `@/shell`).
11
+ */
12
+ import { type ShellState, ViewFrame } from '@/shell'
13
+ import { StatusCard } from '@/status'
14
+
15
+ export function StatusView(shell: ShellState) {
16
+ return (
17
+ <ViewFrame shell={shell} title="Status" subline="astrale view">
18
+ {(session, nodeId) =>
19
+ nodeId ? (
20
+ // Keyed by node id so a target hot-swap remounts with fresh state.
21
+ <StatusCard key={nodeId} session={session} nodeId={nodeId} />
22
+ ) : (
23
+ <div className="banner">No target node — open this view from a StatusPage.</div>
24
+ )
25
+ }
26
+ </ViewFrame>
27
+ )
28
+ }
@@ -1,6 +1,5 @@
1
- import { fileURLToPath } from 'node:url'
2
-
3
1
  import viteReact from '@vitejs/plugin-react'
2
+ import { fileURLToPath } from 'node:url'
4
3
  import { defineConfig } from 'vite'
5
4
 
6
5
  /**
@@ -18,7 +17,7 @@ import { defineConfig } from 'vite'
18
17
  * negotiation, redirect following, delegation — all handled by the SDK).
19
18
  *
20
19
  * `@` is aliased to `src/` (mirrors tsconfig `paths`) so feature code imports
21
- * `@/shell`, `@/ui`, `@/monitor`.
20
+ * `@/shell`, `@/ui`, `@/status`.
22
21
  */
23
22
  export default defineConfig({
24
23
  base: '/ui/',
@@ -1,6 +1,5 @@
1
- import { fileURLToPath } from 'node:url'
2
-
3
1
  import viteReact from '@vitejs/plugin-react'
2
+ import { fileURLToPath } from 'node:url'
4
3
  import { defineConfig } from 'vitest/config'
5
4
 
6
5
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Health logic — PURE status rule, no I/O. Maps an observed probe result to the
3
- * monitor's verdict. (Node identity/layout/seed data lives in `./node`; the
4
- * kernel reads/writes live in `runtime/monitor/`; the network probe behind the
5
- * `Prober` port in `integrations/prober/`.)
2
+ * Health logic — PURE status rules, no I/O. `classify` maps one probe's HTTP code
3
+ * to a monitor verdict; `rollup` aggregates a status page's watched monitors into
4
+ * a page verdict. (Node identity/layout/seed data lives in `./node`; the kernel
5
+ * reads/writes live in `runtime/`; the network probe behind the `Prober` port.)
6
6
  */
7
7
 
8
8
  /** A monitor's health verdict. `unknown` is the pre-first-check state. */
@@ -17,3 +17,18 @@ export function classify(statusCode: number): HealthStatus {
17
17
  if (statusCode >= 200 && statusCode < 400) return 'up'
18
18
  return 'down'
19
19
  }
20
+
21
+ /** A status page's rolled-up verdict — `degraded` = only non-critical checks down. */
22
+ export type PageStatus = 'up' | 'degraded' | 'down' | 'unknown'
23
+
24
+ /**
25
+ * Roll up a page's watched monitors into a page verdict: a CRITICAL monitor down
26
+ * ⇒ `down`; any other monitor down ⇒ `degraded`; all up ⇒ `up`; none ⇒ `unknown`.
27
+ * Pure.
28
+ */
29
+ export function rollup(members: readonly { status: string; critical: boolean }[]): PageStatus {
30
+ if (members.length === 0) return 'unknown'
31
+ const down = members.filter((m) => m.status === 'down')
32
+ if (down.some((m) => m.critical)) return 'down'
33
+ return down.length > 0 ? 'degraded' : 'up'
34
+ }
@@ -15,9 +15,10 @@ export const NODE_CREATE = K.Node.createNode.path.method.raw
15
15
  export const FOLDER_CLASS = K.Folder.path.class.raw
16
16
  export const NAME_KEY = K.Named.name.key
17
17
 
18
- /** Domain class paths. */
18
+ /** Domain class/edge paths. */
19
19
  export const MONITOR_CLASS = D.Monitor.path.class.raw
20
- export const DEPENDS_ON_EDGE = D.depends_on.path.class.raw
20
+ export const STATUS_PAGE_CLASS = D.StatusPage.path.class.raw
21
+ export const WATCHES_EDGE = D.watches.path.class.raw
21
22
 
22
23
  /** Qualified storage keys for Monitor node props. */
23
24
  export const MONITOR_KEYS = {
@@ -27,3 +28,14 @@ export const MONITOR_KEYS = {
27
28
  latencyMs: D.Monitor.latencyMs.key,
28
29
  lastCheckedAt: D.Monitor.lastCheckedAt.key,
29
30
  } as const
31
+
32
+ /** Qualified storage key for the StatusPage's rolled-up status. */
33
+ export const PAGE_KEYS = {
34
+ status: D.StatusPage.status.key,
35
+ } as const
36
+
37
+ /** Qualified key for the `watches` edge's `critical` prop. Edge-prop accessors
38
+ * aren't typed yet (an SDK gap), so this casts to read the key. */
39
+ export const WATCHES_KEYS = {
40
+ critical: (D.watches as unknown as { critical: { key: string } }).critical.key,
41
+ } as const
@@ -1,43 +1,41 @@
1
1
  /**
2
- * Monitor node identity — PURE domain constants + slug logic (no I/O): where
3
- * monitors live in the graph, how a new node's slug is formed, and the seed set.
4
- * `runtime/monitor/` consumes these. The IMPURE bits (the slug's entropy suffix,
5
- * the actual node writes) stay in `runtime/`, so this file is deterministic and
6
- * trivially testable.
2
+ * Node identity — PURE domain constants + slug logic (no I/O): where monitors and
3
+ * status pages live in the graph, how a new node's slug is formed, and the seed
4
+ * set. `runtime/` consumes these; the IMPURE bits (the slug's entropy suffix, the
5
+ * node writes) stay in `runtime/`, so this file is deterministic and testable.
7
6
  */
8
7
 
9
- /** Where Monitor nodes live in the graph — a domain layout choice (`seed` creates the folder). */
8
+ /** Where each kind of node lives in the graph (domain layout; `seed` creates the folders). */
10
9
  export const MONITORS_PARENT = '/monitors'
10
+ export const STATUS_PAGES_PARENT = '/status-pages'
11
11
 
12
- /**
13
- * Deterministic URL-safe stem for a monitor's node slug (host + path, lowercased,
14
- * non-alnum → `-`). Pure.
15
- */
16
- export function slugForUrl(url: string): string {
12
+ /** Deterministic URL-/name-safe stem (lowercased, non-alnum → `-`, clamped). Pure. */
13
+ export function slugify(text: string): string {
17
14
  return (
18
- url
15
+ text
19
16
  .replace(/^https?:\/\//i, '')
20
17
  .toLowerCase()
21
18
  .replace(/[^a-z0-9]+/g, '-')
22
19
  .replace(/^-|-$/g, '')
23
- .slice(0, 40) || 'monitor'
20
+ .slice(0, 40) || 'node'
24
21
  )
25
22
  }
26
23
 
27
24
  /**
28
- * Slug for a NEW monitor node: the url stem + a caller-supplied `suffix`. Pure —
29
- * `runtime/monitor/watch` injects the `suffix` (clock/RNG entropy for collision
30
- * safety) so core stays deterministic. `seed` doesn't use this (it pins fixed
31
- * slugs for idempotency); `watch` does.
25
+ * Slug for a NEW node: the stem + a caller-supplied `suffix`. Pure — `runtime/`
26
+ * injects the `suffix` (clock/RNG entropy for collision safety) so core stays
27
+ * deterministic. `seed` uses fixed slugs (not this) for idempotency.
32
28
  */
33
- export function monitorSlug(url: string, suffix: string): string {
34
- return `${slugForUrl(url)}-${suffix}`
29
+ export function uniqueSlug(text: string, suffix: string): string {
30
+ return `${slugify(text)}-${suffix}`
35
31
  }
36
32
 
37
33
  export interface StarterMonitor {
38
34
  slug: string
39
35
  name: string
40
36
  url: string
37
+ /** Whether the seeded status page treats this monitor as critical. */
38
+ critical: boolean
41
39
  }
42
40
 
43
41
  /**
@@ -46,6 +44,14 @@ export interface StarterMonitor {
46
44
  * a real site. Fixed slugs keep `seed` idempotent across reinstalls.
47
45
  */
48
46
  export const STARTERS: readonly StarterMonitor[] = [
49
- { slug: 'astrale', name: 'Astrale', url: 'https://astrale.ai' },
50
- { slug: 'httpbin-200', name: 'httpbin (200)', url: 'https://httpbin.org/status/200' },
47
+ { slug: 'astrale', name: 'Astrale', url: 'https://astrale.ai', critical: true },
48
+ {
49
+ slug: 'httpbin-200',
50
+ name: 'httpbin (200)',
51
+ url: 'https://httpbin.org/status/200',
52
+ critical: false,
53
+ },
51
54
  ]
55
+
56
+ /** The status page `seed` creates, watching every starter monitor. */
57
+ export const STARTER_PAGE = { slug: 'status', name: 'Status' } as const
package/template/deps.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { Env } from './env'
2
+
1
3
  /**
2
4
  * env → handler dependency container — the ONE place the worker env becomes the
3
5
  * typed `ctx.deps` every method reads. `defineDomain({ deps })` mounts it; the
@@ -14,7 +16,6 @@
14
16
  * of raw config — that's the whole point of this seam.
15
17
  */
16
18
  import { buildProberRegistry, type ProberRegistry } from './integrations/prober/registry'
17
- import type { Env } from './env'
18
19
 
19
20
  /** Typed dependency container handed to every method as `ctx.deps`. */
20
21
  export interface Deps extends ProberRegistry {}
@@ -8,35 +8,24 @@
8
8
  import type { Prober, ProbeResult } from './port'
9
9
 
10
10
  export interface HttpProberConfig {
11
- /** Probe verb — `GET` (default) or the lighter `HEAD`. */
12
- method?: 'GET' | 'HEAD'
13
11
  /** Per-probe timeout in ms (default 10000). */
14
12
  timeoutMs?: number
15
13
  }
16
14
 
17
15
  const DEFAULT_TIMEOUT_MS = 10_000
18
16
 
19
- /** Build the keyless HTTP prober. */
17
+ /** Build the keyless HTTP prober (a `GET` with a timeout). */
20
18
  export function createHttpProber(config: HttpProberConfig = {}): Prober {
21
- const method = config.method ?? 'GET'
22
19
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
23
20
  return {
24
21
  async probe(url): Promise<ProbeResult> {
25
22
  const start = Date.now()
26
23
  try {
27
- const res = await fetch(url, {
28
- method,
29
- redirect: 'follow',
30
- signal: AbortSignal.timeout(timeoutMs),
31
- })
32
- return {
33
- statusCode: res.status,
34
- latencyMs: Date.now() - start,
35
- ok: res.status >= 200 && res.status < 400,
36
- }
24
+ const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(timeoutMs) })
25
+ return { statusCode: res.status, latencyMs: Date.now() - start }
37
26
  } catch {
38
27
  // Unreachable / timeout / DNS failure — a `down` result, not an error.
39
- return { statusCode: 0, latencyMs: Date.now() - start, ok: false }
28
+ return { statusCode: 0, latencyMs: Date.now() - start }
40
29
  }
41
30
  },
42
31
  }
@@ -9,11 +9,7 @@ import type { Prober, ProbeResult } from './port'
9
9
  export function createMockProber(opts: { statusCode?: number; latencyMs?: number } = {}): Prober {
10
10
  const statusCode = opts.statusCode ?? 200
11
11
  const latencyMs = opts.latencyMs ?? 1
12
- const result: ProbeResult = {
13
- statusCode,
14
- latencyMs,
15
- ok: statusCode >= 200 && statusCode < 400,
16
- }
12
+ const result: ProbeResult = { statusCode, latencyMs }
17
13
  return {
18
14
  probe() {
19
15
  return Promise.resolve(result)
@@ -14,8 +14,6 @@ export interface ProbeResult {
14
14
  statusCode: number
15
15
  /** Round-trip time in ms. */
16
16
  latencyMs: number
17
- /** Convenience flag — `statusCode` in `[200, 400)`. */
18
- ok: boolean
19
17
  }
20
18
 
21
19
  export interface Prober {
@@ -43,15 +43,14 @@ export function buildProberRegistry(env: Env): ProberRegistry {
43
43
  }
44
44
  }
45
45
 
46
- const DEFAULT_TIMEOUT_MS = 10_000
47
-
48
46
  /** Bind the abstract prober port to the concrete adapter for this env + target. */
49
47
  function selectProber(env: Env, target?: ProbeTarget): Prober {
50
48
  if (target && isLocalTarget(target.url)) {
51
49
  return createMockProber()
52
50
  }
53
- // Default: the real, keyless HTTP prober.
54
- return createHttpProber({ timeoutMs: parseIntOr(env.PROBE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS) })
51
+ // Default: the real, keyless HTTP prober. Pass the env override (if any); the
52
+ // adapter owns the default timeout, so there's no second copy of it here.
53
+ return createHttpProber({ timeoutMs: parsePositiveInt(env.PROBE_TIMEOUT_MS) })
55
54
  }
56
55
 
57
56
  /** A loopback / unspecified host the public edge can't reach. */
@@ -59,8 +58,8 @@ function isLocalTarget(url: string): boolean {
59
58
  return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url)
60
59
  }
61
60
 
62
- function parseIntOr(value: string | undefined, fallback: number): number {
63
- if (!value) return fallback
61
+ function parsePositiveInt(value: string | undefined): number | undefined {
62
+ if (!value) return undefined
64
63
  const n = Number.parseInt(value, 10)
65
- return Number.isFinite(n) && n > 0 ? n : fallback
64
+ return Number.isFinite(n) && n > 0 ? n : undefined
66
65
  }
@@ -14,10 +14,10 @@
14
14
  "typecheck": "tsgo --noEmit"
15
15
  },
16
16
  "dependencies": {
17
- "@astrale-os/adapter-cloudflare": ">=0.1.10 <1.0.0",
17
+ "@astrale-os/adapter-cloudflare": ">=0.2.1 <1.0.0",
18
18
  "@astrale-os/kernel-core": ">=0.4.3 <1.0.0",
19
19
  "@astrale-os/kernel-dsl": ">=0.1.2 <1.0.0",
20
- "@astrale-os/sdk": ">=0.1.7 <1.0.0",
20
+ "@astrale-os/sdk": ">=0.1.9 <1.0.0",
21
21
  "@hono/node-server": "^1.19.0",
22
22
  "zod": "^4.3.6"
23
23
  },
@@ -23,6 +23,7 @@ allowBuilds:
23
23
  '@astrale-os/kernel-dsl': false
24
24
  '@astrale-os/kernel-server': false
25
25
  '@astrale-os/sdk': false
26
+ '@astrale-os/shell': false
26
27
 
27
28
  # @astrale-os packages ship a guarded no-op preinstall (a workspace-dev guard
28
29
  # that exits instantly outside the monorepo); declare them intentionally
@@ -34,6 +35,7 @@ ignoredBuiltDependencies:
34
35
  - '@astrale-os/kernel-dsl'
35
36
  - '@astrale-os/kernel-server'
36
37
  - '@astrale-os/sdk'
38
+ - '@astrale-os/shell'
37
39
 
38
40
  # pnpm v11 gates `pnpm run` on a deps-status check that misfires on the
39
41
  # ignored preinstalls above.