@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
|
@@ -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/
|
|
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/
|
|
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'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/** Run a
|
|
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 '../
|
|
4
|
+
import { check } from '../status.api'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Wrap `@<id>::check` in the shared `useCapability` lifecycle (idle → running →
|
package/template/client/src/{monitor/hooks/useMonitor.query.ts → status/hooks/useCheckable.query.ts}
RENAMED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/** Load a
|
|
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 {
|
|
6
|
+
import type { CheckableRecord } from '../status.types'
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { checkableFromNode } from '../status.mappers'
|
|
9
9
|
|
|
10
|
-
export type
|
|
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?:
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
4
|
-
*
|
|
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`, `@/
|
|
20
|
+
* `@/shell`, `@/ui`, `@/status`.
|
|
22
21
|
*/
|
|
23
22
|
export default defineConfig({
|
|
24
23
|
base: '/ui/',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Health logic — PURE status
|
|
3
|
-
* monitor
|
|
4
|
-
*
|
|
5
|
-
* `Prober` port
|
|
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
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `runtime
|
|
5
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) || '
|
|
20
|
+
.slice(0, 40) || 'node'
|
|
24
21
|
)
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
|
-
* Slug for a NEW
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
34
|
-
return `${
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
|
63
|
-
if (!value) return
|
|
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 :
|
|
64
|
+
return Number.isFinite(n) && n > 0 ? n : undefined
|
|
66
65
|
}
|
package/template/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"typecheck": "tsgo --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@astrale-os/adapter-cloudflare": ">=0.
|
|
17
|
+
"@astrale-os/adapter-cloudflare": ">=0.2.0 <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
20
|
"@astrale-os/sdk": ">=0.1.7 <1.0.0",
|