@astrale-os/adapter-cloudflare 0.3.1 → 0.3.2
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/LICENSE +202 -0
- package/package.json +4 -4
- package/template/.agents/skills/astrale-cli/SKILL.md +633 -0
- package/template/.domain-studio/comments.json +4 -0
- package/template/.env.example +3 -6
- package/template/CLAUDE.md +8 -6
- package/template/README.md +28 -20
- package/template/client/__tests__/app.test.tsx +39 -248
- package/template/client/__tests__/harness.ts +0 -47
- package/template/client/__tests__/shell.test.ts +46 -0
- package/template/client/package.json +1 -1
- package/template/client/src/app.tsx +6 -8
- package/template/client/src/shell/index.ts +1 -0
- package/template/client/src/shell/transformers.ts +36 -33
- package/template/client/src/shell/use-node.ts +21 -13
- package/template/client/src/ui/index.ts +4 -7
- package/template/core/README.md +23 -0
- package/template/deps.ts +10 -9
- package/template/domain.ts +11 -9
- package/template/env.ts +3 -5
- package/template/functions/index.ts +14 -37
- package/template/icons.ts +25 -0
- package/template/integrations/README.md +22 -0
- package/template/package.json +3 -3
- package/template/runtime/index.ts +25 -72
- package/template/schema/index.ts +10 -10
- package/template/tsconfig.json +1 -0
- package/template/views/index.ts +15 -12
- package/template/client/__tests__/kernel.test.ts +0 -77
- package/template/client/__tests__/seam.test.tsx +0 -115
- package/template/client/src/status/components/StatusCard.tsx +0 -160
- package/template/client/src/status/components/index.ts +0 -1
- package/template/client/src/status/hooks/index.ts +0 -3
- package/template/client/src/status/hooks/useCheck.mutation.ts +0 -16
- package/template/client/src/status/hooks/useCheckable.query.ts +0 -72
- package/template/client/src/status/index.ts +0 -7
- package/template/client/src/status/status.api.ts +0 -29
- package/template/client/src/status/status.mappers.ts +0 -102
- package/template/client/src/status/status.types.ts +0 -26
- package/template/client/src/ui/StatusBadge.tsx +0 -31
- package/template/client/src/views/status.tsx +0 -28
- package/template/core/monitor/health.ts +0 -34
- package/template/core/monitor/index.ts +0 -9
- package/template/core/monitor/keys.ts +0 -41
- package/template/core/monitor/node.ts +0 -63
- package/template/integrations/prober/http.ts +0 -32
- package/template/integrations/prober/mock.ts +0 -18
- package/template/integrations/prober/port.ts +0 -26
- package/template/integrations/prober/registry.ts +0 -65
- package/template/runtime/monitoring/index.ts +0 -8
- package/template/runtime/monitoring/monitor/check.ts +0 -29
- package/template/runtime/monitoring/monitor/index.ts +0 -10
- package/template/runtime/monitoring/monitor/seed.ts +0 -104
- package/template/runtime/monitoring/monitor/watch.ts +0 -31
- package/template/runtime/monitoring/page/add.ts +0 -21
- package/template/runtime/monitoring/page/check.ts +0 -50
- package/template/runtime/monitoring/page/create.ts +0 -24
- package/template/runtime/monitoring/page/index.ts +0 -9
- package/template/runtime/shared.ts +0 -21
- package/template/schema/monitor.ts +0 -92
- package/template/views/status-page.ts +0 -16
- package/template/views/welcome.ts +0 -35
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the presentation/logic seam unlocks:
|
|
3
|
-
*
|
|
4
|
-
* 1. PRESENTATION components (`@/ui`) test with ZERO infrastructure — no fake
|
|
5
|
-
* shell, no fake kernel, no session. Just props in, DOM out. (Contrast
|
|
6
|
-
* app.test.tsx, which stands up a handshake + kernel stub to reach the same
|
|
7
|
-
* markup.)
|
|
8
|
-
* 2. FEATURE hooks (`@/status`) test against a bare `KernelClient` (the fake
|
|
9
|
-
* kernel fetch stub) with no handshake — the write lifecycle and the
|
|
10
|
-
* reload signal are verified once, in isolation.
|
|
11
|
-
*/
|
|
12
|
-
import { act, cleanup, render, renderHook, screen, waitFor } from '@testing-library/react'
|
|
13
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
14
|
-
|
|
15
|
-
import { useCheck, useCheckable } from '@/status'
|
|
16
|
-
import { StatusBadge } from '@/ui'
|
|
17
|
-
|
|
18
|
-
import { checkableNode, fakeKernelSession, installFakeKernel, ok } from './harness'
|
|
19
|
-
|
|
20
|
-
afterEach(cleanup)
|
|
21
|
-
|
|
22
|
-
describe('presentation is pure (no kernel, no session, no shell)', () => {
|
|
23
|
-
it('StatusBadge renders the label and tone class for each status', () => {
|
|
24
|
-
const { rerender } = render(<StatusBadge status="up" />)
|
|
25
|
-
expect(screen.getByText('UP').className).toContain('status-up')
|
|
26
|
-
|
|
27
|
-
rerender(<StatusBadge status="degraded" />)
|
|
28
|
-
expect(screen.getByText('DEGRADED').className).toContain('status-degraded')
|
|
29
|
-
|
|
30
|
-
rerender(<StatusBadge status="down" />)
|
|
31
|
-
expect(screen.getByText('DOWN').className).toContain('status-down')
|
|
32
|
-
|
|
33
|
-
rerender(<StatusBadge status="weird" />)
|
|
34
|
-
expect(screen.getByText('UNKNOWN').className).toContain('status-unknown')
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
describe('useCheckable — query + reload signal', () => {
|
|
39
|
-
it('loads the record, then exposes reloading across a reload()', async () => {
|
|
40
|
-
const session = fakeKernelSession()
|
|
41
|
-
let status = 'down'
|
|
42
|
-
const kernel = installFakeKernel(() => ok(checkableNode({ id: 'page-1', name: 'API', status })))
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const { result } = renderHook(() => useCheckable(session, 'page-1'))
|
|
46
|
-
await waitFor(() => expect(result.current.state).toBe('ok'))
|
|
47
|
-
expect(result.current.record?.status).toBe('down')
|
|
48
|
-
expect(result.current.reloading).toBe(false)
|
|
49
|
-
|
|
50
|
-
// Server-side status flips; a reload picks it up and flags `reloading`.
|
|
51
|
-
status = 'up'
|
|
52
|
-
act(() => {
|
|
53
|
-
result.current.reload()
|
|
54
|
-
})
|
|
55
|
-
expect(result.current.reloading).toBe(true)
|
|
56
|
-
|
|
57
|
-
await waitFor(() => expect(result.current.reloading).toBe(false))
|
|
58
|
-
expect(result.current.record?.status).toBe('up')
|
|
59
|
-
} finally {
|
|
60
|
-
kernel.restore()
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('surfaces a node-load error', async () => {
|
|
65
|
-
const session = fakeKernelSession()
|
|
66
|
-
const kernel = installFakeKernel(() => ({ error: { code: 3002, message: 'Path not found' } }))
|
|
67
|
-
try {
|
|
68
|
-
const { result } = renderHook(() => useCheckable(session, 'missing'))
|
|
69
|
-
await waitFor(() => expect(result.current.state).toBe('error'))
|
|
70
|
-
expect(result.current.message).toMatch(/Path not found/)
|
|
71
|
-
} finally {
|
|
72
|
-
kernel.restore()
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('useCheck — the shared write-lifecycle', () => {
|
|
78
|
-
it('idle → running → done, then resolves true', async () => {
|
|
79
|
-
const session = fakeKernelSession()
|
|
80
|
-
const kernel = installFakeKernel(() => ok(null))
|
|
81
|
-
try {
|
|
82
|
-
const { result } = renderHook(() => useCheck(session, 'page-1'))
|
|
83
|
-
expect(result.current.phase).toBe('idle')
|
|
84
|
-
|
|
85
|
-
let returned: boolean | undefined
|
|
86
|
-
await act(async () => {
|
|
87
|
-
returned = await result.current.run()
|
|
88
|
-
})
|
|
89
|
-
expect(returned).toBe(true)
|
|
90
|
-
expect(result.current.phase).toBe('done')
|
|
91
|
-
expect(result.current.error).toBeNull()
|
|
92
|
-
} finally {
|
|
93
|
-
kernel.restore()
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('captures a failure: phase failed, error set, resolves false', async () => {
|
|
98
|
-
const session = fakeKernelSession()
|
|
99
|
-
const kernel = installFakeKernel(() => ({
|
|
100
|
-
error: { code: 2004, message: 'Permission denied' },
|
|
101
|
-
}))
|
|
102
|
-
try {
|
|
103
|
-
const { result } = renderHook(() => useCheck(session, 'page-1'))
|
|
104
|
-
let returned: boolean | undefined
|
|
105
|
-
await act(async () => {
|
|
106
|
-
returned = await result.current.run()
|
|
107
|
-
})
|
|
108
|
-
expect(returned).toBe(false)
|
|
109
|
-
expect(result.current.phase).toBe('failed')
|
|
110
|
-
expect(result.current.error).toMatch(/Permission denied/)
|
|
111
|
-
} finally {
|
|
112
|
-
kernel.restore()
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
})
|
|
@@ -1,160 +0,0 @@
|
|
|
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 {
|
|
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'
|
|
25
|
-
|
|
26
|
-
import { useCheck, useCheckable } from '../hooks'
|
|
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
|
-
|
|
42
|
-
export function StatusCard({ session, nodeId }: { session: KernelClient; nodeId: string }) {
|
|
43
|
-
const checkable = useCheckable(session, nodeId)
|
|
44
|
-
const check = useCheck(session, nodeId)
|
|
45
|
-
|
|
46
|
-
async function checkNow() {
|
|
47
|
-
if (await check.run()) checkable.reload()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (checkable.state === 'idle' || checkable.state === 'loading') {
|
|
51
|
-
return <Spinner label="Loading…" />
|
|
52
|
-
}
|
|
53
|
-
if (checkable.state === 'error') {
|
|
54
|
-
return <ErrorBanner>Failed to load: {checkable.message}</ErrorBanner>
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const record = checkable.record!
|
|
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)
|
|
64
|
-
const checkButton = (
|
|
65
|
-
<button type="button" className="check-btn" onClick={checkNow} disabled={busy}>
|
|
66
|
-
{checking ? 'Checking...' : checkable.reloading ? 'Refreshing...' : 'Check now'}
|
|
67
|
-
</button>
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Panel title={record.name} actions={checkButton}>
|
|
72
|
-
{check.phase === 'failed' && check.error && (
|
|
73
|
-
<ErrorBanner>Check failed: {check.error}</ErrorBanner>
|
|
74
|
-
)}
|
|
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>
|
|
124
|
-
</div>
|
|
125
|
-
</Panel>
|
|
126
|
-
)
|
|
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 +0,0 @@
|
|
|
1
|
-
export { StatusCard } from './StatusCard'
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** Run a checkable node's `check` — the feature's one write capability. */
|
|
2
|
-
import { type Capability, type KernelClient, useCapability } from '@/shell'
|
|
3
|
-
|
|
4
|
-
import { check } from '../status.api'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Wrap `@<id>::check` in the shared `useCapability` lifecycle (idle → running →
|
|
8
|
-
* done/failed). The view runs it and, on success, reloads the node so the fresh
|
|
9
|
-
* status renders:
|
|
10
|
-
*
|
|
11
|
-
* const probe = useCheck(session, nodeId)
|
|
12
|
-
* if (await probe.run()) reload()
|
|
13
|
-
*/
|
|
14
|
-
export function useCheck(session: KernelClient, nodeId: string): Capability {
|
|
15
|
-
return useCapability(() => check(session, nodeId))
|
|
16
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/** Load a status page with its watched monitors, with reload for post-check refresh. */
|
|
2
|
-
|
|
3
|
-
import { type KernelClient, type KernelNode, useAsync } from '@/shell'
|
|
4
|
-
|
|
5
|
-
import type { StatusPanelRecord, WatchedMonitorRecord } from '../status.types'
|
|
6
|
-
|
|
7
|
-
import { getCheckable, getLinks, getNodeRef } from '../status.api'
|
|
8
|
-
import { checkableFromNode, monitorFromRef, watchedMonitorRefs } from '../status.mappers'
|
|
9
|
-
|
|
10
|
-
export type CheckableQuery = {
|
|
11
|
-
/** Lifecycle of the underlying page + monitor graph read. */
|
|
12
|
-
state: 'idle' | 'loading' | 'error' | 'ok'
|
|
13
|
-
/** The projected record once `ok`. */
|
|
14
|
-
record?: StatusPanelRecord
|
|
15
|
-
/** Failure message once `error`. */
|
|
16
|
-
message?: string
|
|
17
|
-
/** Re-fetch the page and watched monitors after `check` mutates them. */
|
|
18
|
-
reload(): void
|
|
19
|
-
/** True while a `reload()`-triggered re-fetch is in flight. */
|
|
20
|
-
reloading: boolean
|
|
21
|
-
}
|
|
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
|
-
|
|
44
|
-
/**
|
|
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.
|
|
48
|
-
*/
|
|
49
|
-
export function useCheckable(session: KernelClient, nodeId: string): CheckableQuery {
|
|
50
|
-
const resource = useAsync(() => loadStatusPanel(session, nodeId), [session, nodeId])
|
|
51
|
-
|
|
52
|
-
switch (resource.state.status) {
|
|
53
|
-
case 'ok':
|
|
54
|
-
return {
|
|
55
|
-
state: 'ok',
|
|
56
|
-
record: resource.state.data,
|
|
57
|
-
reload: resource.reload,
|
|
58
|
-
reloading: resource.reloading,
|
|
59
|
-
}
|
|
60
|
-
case 'error':
|
|
61
|
-
return {
|
|
62
|
-
state: 'error',
|
|
63
|
-
message: resource.state.message,
|
|
64
|
-
reload: resource.reload,
|
|
65
|
-
reloading: resource.reloading,
|
|
66
|
-
}
|
|
67
|
-
case 'loading':
|
|
68
|
-
return { state: 'loading', reload: resource.reload, reloading: resource.reloading }
|
|
69
|
-
default:
|
|
70
|
-
return { state: 'idle', reload: resource.reload, reloading: resource.reloading }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
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, watchedMonitorRefs } from './status.mappers'
|
|
7
|
-
export type { CheckableRecord, StatusPanelRecord, WatchedMonitorRecord } from './status.types'
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** Raw kernel calls for the status feature — node instance methods. */
|
|
2
|
-
import { callMethod, 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
|
-
}
|
|
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,102 +0,0 @@
|
|
|
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'
|
|
10
|
-
|
|
11
|
-
import type { CheckableRecord, WatchedMonitorRecord } from './status.types'
|
|
12
|
-
|
|
13
|
-
const lastSegment = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
|
|
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
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Project a `Checkable` `KernelNode` into a `CheckableRecord`: the name from the
|
|
27
|
-
* kernel `Named.name` key, plus any monitor details the node carries.
|
|
28
|
-
*/
|
|
29
|
-
export function checkableFromNode(node: KernelNode): CheckableRecord {
|
|
30
|
-
const p = node.props ?? {}
|
|
31
|
-
return {
|
|
32
|
-
id: node.id,
|
|
33
|
-
path: node.path ?? `@${node.id}`,
|
|
34
|
-
className: classShortName(node),
|
|
35
|
-
name: readProp(p, PROP.named.name) ?? lastSegment(node.path ?? '') ?? node.id,
|
|
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,
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** The status feature's client types — what graph nodes project to. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Client-side projection of the `Checkable` node the view renders (a StatusPage).
|
|
5
|
-
* `status` stays a plain string — the subject's verdict
|
|
6
|
-
* (up/degraded/down/unknown), which `StatusBadge` renders.
|
|
7
|
-
*/
|
|
8
|
-
export type CheckableRecord = {
|
|
9
|
-
id: string
|
|
10
|
-
path: string
|
|
11
|
-
className: string
|
|
12
|
-
name: string
|
|
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[]
|
|
26
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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,28 +0,0 @@
|
|
|
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 page" subline="monitoring 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,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
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
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** A monitor's health verdict. `unknown` is the pre-first-check state. */
|
|
9
|
-
export type HealthStatus = 'up' | 'down' | 'unknown'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Map an observed HTTP status code to a verdict. `0` means the host was
|
|
13
|
-
* unreachable (DNS/timeout/refused) — that's `down`. 2xx/3xx is `up`; anything
|
|
14
|
-
* else (4xx/5xx) is `down`. Pure.
|
|
15
|
-
*/
|
|
16
|
-
export function classify(statusCode: number): HealthStatus {
|
|
17
|
-
if (statusCode >= 200 && statusCode < 400) return 'up'
|
|
18
|
-
return 'down'
|
|
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
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The monitoring bounded context's PURE core, in one place: schema accessors
|
|
3
|
-
* (`keys`), the health status rule (`health`), and node identity/layout/seed
|
|
4
|
-
* data (`node`). No I/O, no clock/RNG — all deterministic + testable.
|
|
5
|
-
* `runtime/monitoring/` imports the operations' logic from here.
|
|
6
|
-
*/
|
|
7
|
-
export * from './keys'
|
|
8
|
-
export * from './health'
|
|
9
|
-
export * from './node'
|