@astrale-os/adapter-cloudflare 0.2.0 → 0.3.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.
Files changed (65) hide show
  1. package/dist/build.d.ts +2 -1
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/build.js +1 -1
  4. package/dist/build.js.map +1 -1
  5. package/dist/client.d.ts +14 -13
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +25 -18
  8. package/dist/client.js.map +1 -1
  9. package/dist/cloudflare.d.ts +4 -1
  10. package/dist/cloudflare.d.ts.map +1 -1
  11. package/dist/cloudflare.js +40 -18
  12. package/dist/cloudflare.js.map +1 -1
  13. package/dist/codegen/worker.d.ts +3 -3
  14. package/dist/codegen/worker.d.ts.map +1 -1
  15. package/dist/codegen/worker.js +19 -8
  16. package/dist/codegen/worker.js.map +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +2 -2
  19. package/dist/params.d.ts +3 -0
  20. package/dist/params.d.ts.map +1 -1
  21. package/package.json +2 -2
  22. package/src/build.ts +2 -1
  23. package/src/client.ts +43 -18
  24. package/src/cloudflare.ts +41 -14
  25. package/src/codegen/worker.ts +19 -8
  26. package/src/index.ts +2 -2
  27. package/src/params.ts +4 -0
  28. package/template/CLAUDE.md +24 -0
  29. package/template/README.md +24 -15
  30. package/template/astrale.config.ts +4 -4
  31. package/template/client/README.md +9 -9
  32. package/template/client/__tests__/app.test.tsx +58 -19
  33. package/template/client/__tests__/harness.ts +16 -0
  34. package/template/client/__tests__/kernel.test.ts +23 -3
  35. package/template/client/src/shell/use-async.ts +4 -1
  36. package/template/client/src/status/components/StatusCard.tsx +115 -5
  37. package/template/client/src/status/hooks/useCheckable.query.ts +48 -40
  38. package/template/client/src/status/index.ts +2 -2
  39. package/template/client/src/status/status.api.ts +18 -1
  40. package/template/client/src/status/status.mappers.ts +89 -6
  41. package/template/client/src/status/status.types.ts +17 -2
  42. package/template/client/src/styles.css +235 -14
  43. package/template/client/src/views/status.tsx +1 -1
  44. package/template/core/monitor/index.ts +2 -2
  45. package/template/core/monitor/node.ts +12 -6
  46. package/template/domain.ts +6 -4
  47. package/template/functions/index.ts +31 -7
  48. package/template/package.json +2 -2
  49. package/template/pnpm-lock.yaml +57 -43
  50. package/template/pnpm-workspace.yaml +2 -0
  51. package/template/runtime/index.ts +8 -17
  52. package/template/runtime/monitoring/index.ts +8 -0
  53. package/template/runtime/{monitor → monitoring/monitor}/check.ts +3 -3
  54. package/template/runtime/{monitor → monitoring/monitor}/index.ts +3 -2
  55. package/template/runtime/{monitor → monitoring/monitor}/seed.ts +19 -10
  56. package/template/runtime/{monitor → monitoring/monitor}/watch.ts +5 -5
  57. package/template/runtime/{status-page → monitoring/page}/add.ts +2 -2
  58. package/template/runtime/{status-page → monitoring/page}/check.ts +2 -2
  59. package/template/runtime/{status-page → monitoring/page}/create.ts +3 -3
  60. package/template/runtime/monitoring/page/index.ts +9 -0
  61. package/template/schema/monitor.ts +6 -8
  62. package/template/views/index.ts +1 -1
  63. package/template/.agents/skills/astrale-cli/SKILL.md +0 -458
  64. package/template/.agents/skills/astrale-domain/SKILL.md +0 -371
  65. package/template/runtime/status-page/index.ts +0 -8
@@ -10,10 +10,35 @@
10
10
  */
11
11
  import type { KernelClient } from '@/shell'
12
12
 
13
- import { ErrorBanner, Panel, Spinner, StatusBadge } from '@/ui'
13
+ import {
14
+ EmptyState,
15
+ ErrorBanner,
16
+ ExternalLink,
17
+ Mono,
18
+ Panel,
19
+ relativeTime,
20
+ Spinner,
21
+ StatusBadge,
22
+ } from '@/ui'
23
+
24
+ import type { WatchedMonitorRecord } from '../status.types'
14
25
 
15
26
  import { useCheck, useCheckable } from '../hooks'
16
27
 
28
+ function latestCheckedAt(monitors: WatchedMonitorRecord[]): string | undefined {
29
+ let latest = 0
30
+ let latestIso: string | undefined
31
+ for (const monitor of monitors) {
32
+ if (!monitor.lastCheckedAt) continue
33
+ const time = Date.parse(monitor.lastCheckedAt)
34
+ if (!Number.isNaN(time) && time > latest) {
35
+ latest = time
36
+ latestIso = monitor.lastCheckedAt
37
+ }
38
+ }
39
+ return latestIso
40
+ }
41
+
17
42
  export function StatusCard({ session, nodeId }: { session: KernelClient; nodeId: string }) {
18
43
  const checkable = useCheckable(session, nodeId)
19
44
  const check = useCheck(session, nodeId)
@@ -31,9 +56,14 @@ export function StatusCard({ session, nodeId }: { session: KernelClient; nodeId:
31
56
 
32
57
  const record = checkable.record!
33
58
  const checking = check.phase === 'running'
59
+ const busy = checking || checkable.reloading
60
+ const monitors = record.monitors
61
+ const criticalCount = monitors.filter((monitor) => monitor.critical).length
62
+ const downCount = monitors.filter((monitor) => monitor.status === 'down').length
63
+ const lastChecked = latestCheckedAt(monitors)
34
64
  const checkButton = (
35
- <button type="button" className="check-btn" onClick={checkNow} disabled={checking}>
36
- {checking ? 'Checking' : 'Check now'}
65
+ <button type="button" className="check-btn" onClick={checkNow} disabled={busy}>
66
+ {checking ? 'Checking...' : checkable.reloading ? 'Refreshing...' : 'Check now'}
37
67
  </button>
38
68
  )
39
69
 
@@ -42,9 +72,89 @@ export function StatusCard({ session, nodeId }: { session: KernelClient; nodeId:
42
72
  {check.phase === 'failed' && check.error && (
43
73
  <ErrorBanner>Check failed: {check.error}</ErrorBanner>
44
74
  )}
45
- <div className="status-row">
46
- <StatusBadge status={record.status} />
75
+ <div className="status-dashboard">
76
+ <div className="status-overview">
77
+ <div>
78
+ <p className="status-label">Overall health</p>
79
+ <StatusBadge status={record.status} />
80
+ </div>
81
+ <div className="summary-grid" aria-label="Monitoring summary">
82
+ <div className="summary-cell">
83
+ <span className="summary-value">{monitors.length}</span>
84
+ <span className="summary-label">monitors</span>
85
+ </div>
86
+ <div className="summary-cell">
87
+ <span className="summary-value">{criticalCount}</span>
88
+ <span className="summary-label">critical</span>
89
+ </div>
90
+ <div className="summary-cell">
91
+ <span className="summary-value">{downCount}</span>
92
+ <span className="summary-label">down</span>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <div className="status-meta">
98
+ <span>Page path</span>
99
+ <Mono value={record.path} />
100
+ <span>Last checked</span>
101
+ <span className="mono" title={lastChecked}>
102
+ {relativeTime(lastChecked)}
103
+ </span>
104
+ </div>
105
+
106
+ <section className="monitor-section" aria-label="Watched monitors">
107
+ <div className="monitor-section-head">
108
+ <h3>Watched monitors</h3>
109
+ {checkable.reloading && <span className="refreshing">Refreshing</span>}
110
+ </div>
111
+
112
+ {monitors.length === 0 ? (
113
+ <EmptyState hint="StatusPage.add({ monitor, critical })">
114
+ This page is not watching any monitors yet.
115
+ </EmptyState>
116
+ ) : (
117
+ <div className="monitor-list">
118
+ {monitors.map((monitor) => (
119
+ <MonitorRow key={`${monitor.path}:${monitor.critical}`} monitor={monitor} />
120
+ ))}
121
+ </div>
122
+ )}
123
+ </section>
47
124
  </div>
48
125
  </Panel>
49
126
  )
50
127
  }
128
+
129
+ function MonitorRow({ monitor }: { monitor: WatchedMonitorRecord }) {
130
+ return (
131
+ <article className="monitor-row">
132
+ <div className="monitor-status">
133
+ <StatusBadge status={monitor.status} />
134
+ <span className={monitor.critical ? 'weight weight-critical' : 'weight'}>
135
+ {monitor.critical ? 'Critical' : 'Standard'}
136
+ </span>
137
+ </div>
138
+
139
+ <div className="monitor-main">
140
+ <h4>{monitor.name}</h4>
141
+ <ExternalLink url={monitor.url} />
142
+ </div>
143
+
144
+ <dl className="monitor-metrics">
145
+ <div>
146
+ <dt>HTTP</dt>
147
+ <dd>{monitor.statusCode ?? '-'}</dd>
148
+ </div>
149
+ <div>
150
+ <dt>Latency</dt>
151
+ <dd>{monitor.latencyMs === undefined ? '-' : `${monitor.latencyMs}ms`}</dd>
152
+ </div>
153
+ <div>
154
+ <dt>Checked</dt>
155
+ <dd title={monitor.lastCheckedAt}>{relativeTime(monitor.lastCheckedAt)}</dd>
156
+ </div>
157
+ </dl>
158
+ </article>
159
+ )
160
+ }
@@ -1,64 +1,72 @@
1
- /** Load a checkable node typed record, with a `reload()` for post-check refresh. */
2
- import { useCallback, useRef, useState } from 'react'
1
+ /** Load a status page with its watched monitors, with reload for post-check refresh. */
3
2
 
4
- import { type KernelClient, useNode } from '@/shell'
3
+ import { type KernelClient, type KernelNode, useAsync } from '@/shell'
5
4
 
6
- import type { CheckableRecord } from '../status.types'
5
+ import type { StatusPanelRecord, WatchedMonitorRecord } from '../status.types'
7
6
 
8
- import { checkableFromNode } from '../status.mappers'
7
+ import { getCheckable, getLinks, getNodeRef } from '../status.api'
8
+ import { checkableFromNode, monitorFromRef, watchedMonitorRefs } from '../status.mappers'
9
9
 
10
10
  export type CheckableQuery = {
11
- /** Lifecycle of the underlying `@<id>::get`. */
11
+ /** Lifecycle of the underlying page + monitor graph read. */
12
12
  state: 'idle' | 'loading' | 'error' | 'ok'
13
13
  /** The projected record once `ok`. */
14
- record?: CheckableRecord
14
+ record?: StatusPanelRecord
15
15
  /** Failure message once `error`. */
16
16
  message?: string
17
- /** Re-fetch the node call after `check` mutates it server-side. */
17
+ /** Re-fetch the page and watched monitors after `check` mutates them. */
18
18
  reload(): void
19
19
  /** True while a `reload()`-triggered re-fetch is in flight. */
20
20
  reloading: boolean
21
21
  }
22
22
 
23
+ async function loadMonitor(
24
+ session: KernelClient,
25
+ ref: ReturnType<typeof watchedMonitorRefs>[number],
26
+ ): Promise<WatchedMonitorRecord> {
27
+ try {
28
+ const node = (await getNodeRef(session, ref.target)) as KernelNode
29
+ return { ...checkableFromNode(node), critical: ref.critical }
30
+ } catch {
31
+ return monitorFromRef(ref)
32
+ }
33
+ }
34
+
35
+ async function loadStatusPanel(session: KernelClient, nodeId: string): Promise<StatusPanelRecord> {
36
+ const page = checkableFromNode((await getCheckable(session, nodeId)) as KernelNode)
37
+ if (page.className !== 'StatusPage') return { ...page, monitors: [] }
38
+
39
+ const refs = watchedMonitorRefs(await getLinks(session, nodeId))
40
+ const monitors = await Promise.all(refs.map((ref) => loadMonitor(session, ref)))
41
+ return { ...page, monitors }
42
+ }
43
+
23
44
  /**
24
- * Load the checkable node `nodeId` and project it to a `CheckableRecord`. Wraps
25
- * `useNode` (which owns the `@<id>::get` + the hot-swap re-fetch) and re-exposes
26
- * its `reload()` so the view can refresh after a `check`. `reloading` is true
27
- * from the moment `reload()` is called until the re-fetch settles — distinct from
28
- * the initial `loading`, so the view can show the existing record meanwhile.
45
+ * Load the status page `nodeId`, then its outgoing `watches` targets. The view
46
+ * calls `reload()` after `::check`, so the rolled-up page status and all monitor
47
+ * rows refresh together.
29
48
  */
30
49
  export function useCheckable(session: KernelClient, nodeId: string): CheckableQuery {
31
- const node = useNode(session, nodeId)
32
- const [reloading, setReloading] = useState(false)
33
- // Two-phase reload tracker. `reload()` arms it as `'requested'`; once we
34
- // observe `useNode` enter `loading` it advances to `'loading'`; the next time
35
- // it leaves `loading` the re-fetch has settled, so we clear `reloading`. The
36
- // two phases avoid clearing on the synchronous render BEFORE the effect runs
37
- // (when the status is still the pre-reload `ok`).
38
- const phase = useRef<'idle' | 'requested' | 'loading'>('idle')
39
-
40
- if (phase.current === 'requested' && node.status === 'loading') {
41
- phase.current = 'loading'
42
- } else if (phase.current === 'loading' && node.status !== 'loading') {
43
- phase.current = 'idle'
44
- if (reloading) setReloading(false)
45
- }
46
-
47
- const nodeReload = node.reload
48
- const reload = useCallback(() => {
49
- phase.current = 'requested'
50
- setReloading(true)
51
- nodeReload()
52
- }, [nodeReload])
50
+ const resource = useAsync(() => loadStatusPanel(session, nodeId), [session, nodeId])
53
51
 
54
- switch (node.status) {
52
+ switch (resource.state.status) {
55
53
  case 'ok':
56
- return { state: 'ok', record: checkableFromNode(node.node), reload, reloading }
54
+ return {
55
+ state: 'ok',
56
+ record: resource.state.data,
57
+ reload: resource.reload,
58
+ reloading: resource.reloading,
59
+ }
57
60
  case 'error':
58
- return { state: 'error', message: node.message, reload, reloading }
61
+ return {
62
+ state: 'error',
63
+ message: resource.state.message,
64
+ reload: resource.reload,
65
+ reloading: resource.reloading,
66
+ }
59
67
  case 'loading':
60
- return { state: 'loading', reload, reloading }
68
+ return { state: 'loading', reload: resource.reload, reloading: resource.reloading }
61
69
  default:
62
- return { state: 'idle', reload, reloading }
70
+ return { state: 'idle', reload: resource.reload, reloading: resource.reloading }
63
71
  }
64
72
  }
@@ -3,5 +3,5 @@
3
3
  export * from './components'
4
4
  export * from './hooks'
5
5
  export { check } from './status.api'
6
- export { checkableFromNode } from './status.mappers'
7
- export type { CheckableRecord } from './status.types'
6
+ export { checkableFromNode, watchedMonitorRefs } from './status.mappers'
7
+ export type { CheckableRecord, StatusPanelRecord, WatchedMonitorRecord } from './status.types'
@@ -1,5 +1,5 @@
1
1
  /** Raw kernel calls for the status feature — node instance methods. */
2
- import { invokeNode, type KernelClient } from '@/shell'
2
+ import { callMethod, invokeNode, type KernelClient } from '@/shell'
3
3
 
4
4
  /**
5
5
  * Run a checkable node's `check` instance method (`@<id>::check`). The single
@@ -10,3 +10,20 @@ import { invokeNode, type KernelClient } from '@/shell'
10
10
  export function check(session: KernelClient, nodeId: string): Promise<unknown> {
11
11
  return invokeNode(session, nodeId, 'check', {})
12
12
  }
13
+
14
+ export function getCheckable(session: KernelClient, nodeId: string): Promise<unknown> {
15
+ return invokeNode(session, nodeId, 'get', {})
16
+ }
17
+
18
+ export function getLinks(session: KernelClient, nodeId: string): Promise<unknown> {
19
+ return invokeNode(session, nodeId, 'getLinks', { direction: 'out' })
20
+ }
21
+
22
+ function getRefMethod(ref: string): string {
23
+ if (ref.startsWith('/') || ref.startsWith('@')) return `${ref}::get`
24
+ return `@${ref}::get`
25
+ }
26
+
27
+ export function getNodeRef(session: KernelClient, ref: string): Promise<unknown> {
28
+ return callMethod(session, getRefMethod(ref), {})
29
+ }
@@ -1,19 +1,102 @@
1
- /** Node typed record transform for the status feature. */
2
- import { type KernelNode, PROP, readProp, readPropBySuffix } from '@/shell'
1
+ /** Kernel graph payloads -> typed records for the status feature. */
2
+ import {
3
+ classShortName,
4
+ type KernelNode,
5
+ PROP,
6
+ qualifiedProp,
7
+ qualifiedString,
8
+ readProp,
9
+ } from '@/shell'
3
10
 
4
- import type { CheckableRecord } from './status.types'
11
+ import type { CheckableRecord, WatchedMonitorRecord } from './status.types'
5
12
 
6
13
  const lastSegment = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
7
14
 
15
+ function numericProp(props: Record<string, unknown>, name: string): number | undefined {
16
+ const value = qualifiedProp(props, name)
17
+ if (typeof value === 'number' && Number.isFinite(value)) return value
18
+ if (typeof value === 'string' && value.trim() !== '') {
19
+ const parsed = Number(value)
20
+ if (Number.isFinite(parsed)) return parsed
21
+ }
22
+ return undefined
23
+ }
24
+
8
25
  /**
9
26
  * 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'`).
27
+ * kernel `Named.name` key, plus any monitor details the node carries.
12
28
  */
13
29
  export function checkableFromNode(node: KernelNode): CheckableRecord {
14
30
  const p = node.props ?? {}
15
31
  return {
32
+ id: node.id,
33
+ path: node.path ?? `@${node.id}`,
34
+ className: classShortName(node),
16
35
  name: readProp(p, PROP.named.name) ?? lastSegment(node.path ?? '') ?? node.id,
17
- status: readPropBySuffix(p, '.property.status') ?? 'unknown',
36
+ status: qualifiedString(p, 'status') ?? 'unknown',
37
+ url: qualifiedString(p, 'url'),
38
+ statusCode: numericProp(p, 'statusCode'),
39
+ latencyMs: numericProp(p, 'latencyMs'),
40
+ lastCheckedAt: qualifiedString(p, 'lastCheckedAt'),
41
+ }
42
+ }
43
+
44
+ type RawLink = {
45
+ class?: unknown
46
+ edgeClass?: unknown
47
+ target?: unknown
48
+ to?: unknown
49
+ props?: Record<string, unknown>
50
+ }
51
+
52
+ function rawString(value: unknown): string | undefined {
53
+ if (typeof value === 'string' && value !== '') return value
54
+ if (value && typeof value === 'object') {
55
+ const raw = (value as { raw?: unknown }).raw
56
+ if (typeof raw === 'string' && raw !== '') return raw
57
+ }
58
+ return undefined
59
+ }
60
+
61
+ function rawLinks(raw: unknown): RawLink[] {
62
+ if (Array.isArray(raw)) return raw as RawLink[]
63
+ const obj = raw as { links?: RawLink[]; edges?: RawLink[] } | null
64
+ return obj?.links ?? obj?.edges ?? []
65
+ }
66
+
67
+ function booleanProp(props: Record<string, unknown> | undefined, name: string): boolean {
68
+ if (!props) return false
69
+ const direct = props[name]
70
+ if (typeof direct === 'boolean') return direct
71
+ return qualifiedProp(props, name) === true
72
+ }
73
+
74
+ export type WatchedMonitorRef = {
75
+ target: string
76
+ critical: boolean
77
+ }
78
+
79
+ /** Parse outgoing StatusPage `watches` edges into target refs and roll-up weight. */
80
+ export function watchedMonitorRefs(raw: unknown): WatchedMonitorRef[] {
81
+ const out: WatchedMonitorRef[] = []
82
+ for (const link of rawLinks(raw)) {
83
+ const cls = rawString(link.class) ?? rawString(link.edgeClass) ?? ''
84
+ if (cls && !cls.endsWith('class.watches') && !cls.endsWith('.watches')) continue
85
+ const target = rawString(link.target) ?? rawString(link.to)
86
+ if (!target) continue
87
+ out.push({ target, critical: booleanProp(link.props, 'critical') })
88
+ }
89
+ return out
90
+ }
91
+
92
+ /** Fallback row when a linked monitor ref exists but the target cannot be loaded. */
93
+ export function monitorFromRef(ref: WatchedMonitorRef): WatchedMonitorRecord {
94
+ return {
95
+ id: ref.target,
96
+ path: ref.target,
97
+ className: 'Monitor',
98
+ name: lastSegment(ref.target.replace(/^@/, '')),
99
+ status: 'unknown',
100
+ critical: ref.critical,
18
101
  }
19
102
  }
@@ -1,11 +1,26 @@
1
- /** The status feature's client type — what a `Checkable` node projects to. */
1
+ /** The status feature's client types — what graph nodes project to. */
2
2
 
3
3
  /**
4
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
5
+ * `status` stays a plain string — the subject's verdict
6
6
  * (up/degraded/down/unknown), which `StatusBadge` renders.
7
7
  */
8
8
  export type CheckableRecord = {
9
+ id: string
10
+ path: string
11
+ className: string
9
12
  name: string
10
13
  status: string
14
+ url?: string
15
+ statusCode?: number
16
+ latencyMs?: number
17
+ lastCheckedAt?: string
18
+ }
19
+
20
+ export type WatchedMonitorRecord = CheckableRecord & {
21
+ critical: boolean
22
+ }
23
+
24
+ export type StatusPanelRecord = CheckableRecord & {
25
+ monitors: WatchedMonitorRecord[]
11
26
  }