@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.
- package/dist/build.d.ts +2 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +14 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +25 -18
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.d.ts +4 -1
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +40 -18
- package/dist/cloudflare.js.map +1 -1
- package/dist/codegen/worker.d.ts +3 -3
- package/dist/codegen/worker.d.ts.map +1 -1
- package/dist/codegen/worker.js +19 -8
- package/dist/codegen/worker.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/params.d.ts +3 -0
- package/dist/params.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/build.ts +2 -1
- package/src/client.ts +43 -18
- package/src/cloudflare.ts +41 -14
- package/src/codegen/worker.ts +19 -8
- package/src/index.ts +2 -2
- package/src/params.ts +4 -0
- package/template/CLAUDE.md +24 -0
- package/template/README.md +24 -15
- package/template/astrale.config.ts +4 -4
- package/template/client/README.md +9 -9
- package/template/client/__tests__/app.test.tsx +58 -19
- package/template/client/__tests__/harness.ts +16 -0
- package/template/client/__tests__/kernel.test.ts +23 -3
- package/template/client/src/shell/use-async.ts +4 -1
- package/template/client/src/status/components/StatusCard.tsx +115 -5
- package/template/client/src/status/hooks/useCheckable.query.ts +48 -40
- package/template/client/src/status/index.ts +2 -2
- package/template/client/src/status/status.api.ts +18 -1
- package/template/client/src/status/status.mappers.ts +89 -6
- package/template/client/src/status/status.types.ts +17 -2
- package/template/client/src/styles.css +235 -14
- package/template/client/src/views/status.tsx +1 -1
- package/template/core/monitor/index.ts +2 -2
- package/template/core/monitor/node.ts +12 -6
- package/template/domain.ts +6 -4
- package/template/functions/index.ts +31 -7
- package/template/package.json +2 -2
- package/template/pnpm-lock.yaml +57 -43
- package/template/pnpm-workspace.yaml +2 -0
- package/template/runtime/index.ts +8 -17
- package/template/runtime/monitoring/index.ts +8 -0
- package/template/runtime/{monitor → monitoring/monitor}/check.ts +3 -3
- package/template/runtime/{monitor → monitoring/monitor}/index.ts +3 -2
- package/template/runtime/{monitor → monitoring/monitor}/seed.ts +19 -10
- package/template/runtime/{monitor → monitoring/monitor}/watch.ts +5 -5
- package/template/runtime/{status-page → monitoring/page}/add.ts +2 -2
- package/template/runtime/{status-page → monitoring/page}/check.ts +2 -2
- package/template/runtime/{status-page → monitoring/page}/create.ts +3 -3
- package/template/runtime/monitoring/page/index.ts +9 -0
- package/template/schema/monitor.ts +6 -8
- package/template/views/index.ts +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +0 -458
- package/template/.agents/skills/astrale-domain/SKILL.md +0 -371
- 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 {
|
|
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={
|
|
36
|
-
{checking ? 'Checking
|
|
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-
|
|
46
|
-
<
|
|
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
|
|
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,
|
|
3
|
+
import { type KernelClient, type KernelNode, useAsync } from '@/shell'
|
|
5
4
|
|
|
6
|
-
import type {
|
|
5
|
+
import type { StatusPanelRecord, WatchedMonitorRecord } from '../status.types'
|
|
7
6
|
|
|
8
|
-
import {
|
|
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
|
|
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?:
|
|
14
|
+
record?: StatusPanelRecord
|
|
15
15
|
/** Failure message once `error`. */
|
|
16
16
|
message?: string
|
|
17
|
-
/** Re-fetch the
|
|
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
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
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
|
|
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 (
|
|
52
|
+
switch (resource.state.status) {
|
|
55
53
|
case 'ok':
|
|
56
|
-
return {
|
|
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 {
|
|
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
|
-
/**
|
|
2
|
-
import {
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
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
|
}
|