@astrale-os/adapter-cloudflare 0.1.8 → 0.1.10
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/assets-pack.d.ts +1 -1
- package/dist/assets-pack.js +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +15 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -1
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.d.ts +15 -3
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +52 -18
- package/dist/cloudflare.js.map +1 -1
- package/dist/codegen/worker.d.ts +26 -6
- package/dist/codegen/worker.d.ts.map +1 -1
- package/dist/codegen/worker.js +67 -54
- package/dist/codegen/worker.js.map +1 -1
- package/dist/codegen/wrangler.d.ts +11 -2
- package/dist/codegen/wrangler.d.ts.map +1 -1
- package/dist/codegen/wrangler.js +11 -5
- package/dist/codegen/wrangler.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/params.d.ts +30 -30
- package/dist/params.d.ts.map +1 -1
- package/dist/parse-output.d.ts +1 -1
- package/dist/parse-output.js +1 -1
- package/package.json +6 -2
- package/src/assets-pack.ts +1 -1
- package/src/build.ts +15 -0
- package/src/client.ts +11 -1
- package/src/cloudflare.ts +53 -18
- package/src/codegen/worker.ts +76 -59
- package/src/codegen/wrangler.ts +15 -5
- package/src/index.ts +6 -3
- package/src/params.ts +32 -31
- package/src/parse-output.ts +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +26 -12
- package/template/.agents/skills/astrale-domain/SKILL.md +46 -29
- package/template/.env.example +6 -0
- package/template/README.md +25 -10
- package/template/astrale.config.ts +27 -33
- package/template/client/README.md +80 -63
- package/template/client/__tests__/app.test.tsx +188 -99
- package/template/client/__tests__/harness.ts +67 -12
- package/template/client/__tests__/kernel.test.ts +65 -50
- package/template/client/__tests__/seam.test.tsx +111 -0
- package/template/client/index.html +1 -1
- package/template/client/package.json +1 -0
- package/template/client/src/app.tsx +40 -83
- package/template/client/src/main.tsx +2 -2
- package/template/client/src/monitor/components/MonitorCard.tsx +50 -0
- package/template/client/src/monitor/components/index.ts +1 -0
- package/template/client/src/monitor/hooks/index.ts +3 -0
- package/template/client/src/monitor/hooks/useCheck.mutation.ts +16 -0
- package/template/client/src/monitor/hooks/useMonitor.query.ts +64 -0
- package/template/client/src/monitor/index.ts +6 -0
- package/template/client/src/monitor/monitor.api.ts +11 -0
- package/template/client/src/monitor/monitor.mappers.ts +38 -0
- package/template/client/src/monitor/monitor.types.ts +23 -0
- package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +38 -0
- package/template/client/src/monitor/ui/StatusBadge.UI.tsx +14 -0
- package/template/client/src/monitor/ui/index.ts +8 -0
- package/template/client/src/shell/client.ts +67 -0
- package/template/client/src/shell/index.ts +20 -0
- package/template/client/src/shell/invoke.ts +35 -0
- package/template/client/src/shell/transformers.ts +72 -0
- package/template/client/src/shell/use-async.ts +56 -0
- package/template/client/src/shell/use-capability.ts +61 -0
- package/template/client/src/shell/use-node.ts +61 -0
- package/template/client/src/shell/use-shell.ts +91 -0
- package/template/client/src/shell/view-router.tsx +98 -0
- package/template/client/src/styles.css +177 -4
- package/template/client/src/ui/format.ts +24 -0
- package/template/client/src/ui/index.ts +9 -0
- package/template/client/src/ui/surface.tsx +56 -0
- package/template/client/src/ui/value.tsx +32 -0
- package/template/client/src/views/monitor.tsx +30 -0
- package/template/client/tsconfig.json +3 -2
- package/template/client/vite.config.ts +14 -15
- package/template/client/vitest.config.ts +12 -5
- package/template/core/monitor/health.ts +19 -0
- package/template/core/monitor/index.ts +9 -0
- package/template/core/monitor/keys.ts +29 -0
- package/template/core/monitor/node.ts +51 -0
- package/template/deps.ts +25 -0
- package/template/domain.ts +33 -0
- package/template/env.ts +4 -0
- package/template/integrations/prober/http.ts +43 -0
- package/template/integrations/prober/mock.ts +22 -0
- package/template/integrations/prober/port.ts +28 -0
- package/template/integrations/prober/registry.ts +66 -0
- package/template/package.json +2 -3
- package/template/pnpm-lock.yaml +2766 -0
- package/template/runtime/index.ts +79 -0
- package/template/runtime/monitor/check.ts +29 -0
- package/template/runtime/monitor/dependsOn.ts +16 -0
- package/template/runtime/monitor/index.ts +12 -0
- package/template/runtime/monitor/seed.ts +74 -0
- package/template/runtime/monitor/shared.ts +17 -0
- package/template/runtime/monitor/watch.ts +37 -0
- package/template/schema/index.ts +13 -4
- package/template/schema/monitor.ts +80 -0
- package/template/tsconfig.json +13 -2
- package/template/views/index.ts +9 -2
- package/template/views/monitor.ts +22 -0
- package/dist/astrale.d.ts +0 -27
- package/dist/astrale.d.ts.map +0 -1
- package/dist/astrale.js +0 -222
- package/dist/astrale.js.map +0 -1
- package/src/astrale.ts +0 -259
- package/template/client/src/lib/kernel.ts +0 -135
- package/template/client/src/lib/shell.ts +0 -197
- package/template/client/src/lib/use-node.ts +0 -66
- package/template/client/src/lib/use-shell.ts +0 -85
- package/template/methods/index.ts +0 -66
- package/template/methods/note.ts +0 -131
- package/template/schema/compiled.ts +0 -14
- package/template/schema/note.ts +0 -64
- package/template/views/note.ts +0 -21
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the presentation/logic seam unlocks:
|
|
3
|
+
*
|
|
4
|
+
* 1. PRESENTATION components (`@/ui` and a feature's own `monitor/ui`) test with
|
|
5
|
+
* ZERO infrastructure — no fake shell, no fake kernel, no session. Just props
|
|
6
|
+
* in, DOM out. (Contrast app.test.tsx, which stands up a handshake + kernel
|
|
7
|
+
* stub to reach the same markup.)
|
|
8
|
+
* 2. FEATURE hooks (`@/monitor`) 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, useMonitor } from '@/monitor'
|
|
16
|
+
import { StatusBadge } from '@/monitor/ui'
|
|
17
|
+
import { fakeKernelSession, installFakeKernel, monitorNode, ok } from './harness'
|
|
18
|
+
|
|
19
|
+
afterEach(cleanup)
|
|
20
|
+
|
|
21
|
+
describe('presentation is pure (no kernel, no session, no shell)', () => {
|
|
22
|
+
it('StatusBadge renders the label and tone class for each status', () => {
|
|
23
|
+
const { rerender } = render(<StatusBadge status="up" />)
|
|
24
|
+
expect(screen.getByText('UP').className).toContain('status-up')
|
|
25
|
+
|
|
26
|
+
rerender(<StatusBadge status="down" />)
|
|
27
|
+
expect(screen.getByText('DOWN').className).toContain('status-down')
|
|
28
|
+
|
|
29
|
+
rerender(<StatusBadge status="unknown" />)
|
|
30
|
+
expect(screen.getByText('UNKNOWN').className).toContain('status-unknown')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('useMonitor — query + reload signal', () => {
|
|
35
|
+
it('loads the record, then exposes reloading across a reload()', async () => {
|
|
36
|
+
const session = fakeKernelSession()
|
|
37
|
+
let status = 'down'
|
|
38
|
+
const kernel = installFakeKernel(() =>
|
|
39
|
+
ok(monitorNode({ id: 'mon-1', name: 'API', url: 'https://x.test', status })),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const { result } = renderHook(() => useMonitor(session, 'mon-1'))
|
|
44
|
+
await waitFor(() => expect(result.current.state).toBe('ok'))
|
|
45
|
+
expect(result.current.record?.status).toBe('down')
|
|
46
|
+
expect(result.current.reloading).toBe(false)
|
|
47
|
+
|
|
48
|
+
// Server-side status flips; a reload picks it up and flags `reloading`.
|
|
49
|
+
status = 'up'
|
|
50
|
+
act(() => {
|
|
51
|
+
result.current.reload()
|
|
52
|
+
})
|
|
53
|
+
expect(result.current.reloading).toBe(true)
|
|
54
|
+
|
|
55
|
+
await waitFor(() => expect(result.current.reloading).toBe(false))
|
|
56
|
+
expect(result.current.record?.status).toBe('up')
|
|
57
|
+
} finally {
|
|
58
|
+
kernel.restore()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('surfaces a node-load error', async () => {
|
|
63
|
+
const session = fakeKernelSession()
|
|
64
|
+
const kernel = installFakeKernel(() => ({ error: { code: 3002, message: 'Path not found' } }))
|
|
65
|
+
try {
|
|
66
|
+
const { result } = renderHook(() => useMonitor(session, 'missing'))
|
|
67
|
+
await waitFor(() => expect(result.current.state).toBe('error'))
|
|
68
|
+
expect(result.current.message).toMatch(/Path not found/)
|
|
69
|
+
} finally {
|
|
70
|
+
kernel.restore()
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('useCheck — the shared write-lifecycle', () => {
|
|
76
|
+
it('idle → running → done, then resolves true', async () => {
|
|
77
|
+
const session = fakeKernelSession()
|
|
78
|
+
const kernel = installFakeKernel(() => ok(null))
|
|
79
|
+
try {
|
|
80
|
+
const { result } = renderHook(() => useCheck(session, 'mon-1'))
|
|
81
|
+
expect(result.current.phase).toBe('idle')
|
|
82
|
+
|
|
83
|
+
let returned: boolean | undefined
|
|
84
|
+
await act(async () => {
|
|
85
|
+
returned = await result.current.run()
|
|
86
|
+
})
|
|
87
|
+
expect(returned).toBe(true)
|
|
88
|
+
expect(result.current.phase).toBe('done')
|
|
89
|
+
expect(result.current.error).toBeNull()
|
|
90
|
+
} finally {
|
|
91
|
+
kernel.restore()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('captures a failure: phase failed, error set, resolves false', async () => {
|
|
96
|
+
const session = fakeKernelSession()
|
|
97
|
+
const kernel = installFakeKernel(() => ({ error: { code: 2004, message: 'Permission denied' } }))
|
|
98
|
+
try {
|
|
99
|
+
const { result } = renderHook(() => useCheck(session, 'mon-1'))
|
|
100
|
+
let returned: boolean | undefined
|
|
101
|
+
await act(async () => {
|
|
102
|
+
returned = await result.current.run()
|
|
103
|
+
})
|
|
104
|
+
expect(returned).toBe(false)
|
|
105
|
+
expect(result.current.phase).toBe('failed')
|
|
106
|
+
expect(result.current.error).toMatch(/Permission denied/)
|
|
107
|
+
} finally {
|
|
108
|
+
kernel.restore()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -1,94 +1,51 @@
|
|
|
1
|
-
import type { NodeSession } from './lib/use-node'
|
|
2
|
-
|
|
3
|
-
import { classShortName, type KernelNode, PROP, readProp, readPropBySuffix } from './lib/kernel'
|
|
4
|
-
import { useNode } from './lib/use-node'
|
|
5
|
-
import { useShell } from './lib/use-shell'
|
|
6
|
-
|
|
7
1
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
2
|
+
* Multi-view router for the domain's one SPA bundle.
|
|
3
|
+
*
|
|
4
|
+
* The domain declares its SPA Views, each served from THIS one bundle under
|
|
5
|
+
* `/ui/*` and mounted by the shell as its own iframe at its own mount path:
|
|
6
|
+
* - `/ui/monitor` → the Monitor detail panel (`views/monitor.tsx`)
|
|
7
|
+
*
|
|
8
|
+
* Because the shell mounts one iframe per view at its mount path, inside the
|
|
9
|
+
* iframe `window.location.pathname` is that path. `App` reads it (via
|
|
10
|
+
* `resolveView`) and renders the matching view; an unregistered path shows a
|
|
11
|
+
* self-describing fallback.
|
|
12
|
+
*
|
|
13
|
+
* To add a view:
|
|
14
|
+
* 1. write a `ViewComponent` (usually wrapping `ViewFrame` — see `views/`),
|
|
15
|
+
* 2. add a `ROUTES` entry below keyed by its mount path,
|
|
16
|
+
* 3. register a matching `defineView({ mount: '/ui/<path>' })` in the domain's
|
|
17
|
+
* `views/` so the shell mounts an iframe there.
|
|
13
18
|
*/
|
|
14
|
-
export function App() {
|
|
15
|
-
const { status, session, nodeId, reason } = useShell()
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div className="wrap">
|
|
19
|
-
<div className="card">
|
|
20
|
-
{status === 'loading' && (
|
|
21
|
-
<>
|
|
22
|
-
<h1 className="title">Note</h1>
|
|
23
|
-
<p className="subline">ui-note · Astrale view SPA</p>
|
|
24
|
-
<p className="muted">Waiting for the shell handshake…</p>
|
|
25
|
-
</>
|
|
26
|
-
)}
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<h1 className="title">Note</h1>
|
|
31
|
-
<p className="subline">ui-note · Astrale view SPA</p>
|
|
32
|
-
<div className="banner">
|
|
33
|
-
No parent shell ({reason}). This view is meant to be mounted by the Astrale GUI, which
|
|
34
|
-
hands it a target node and a kernel token. Showing a standalone preview.
|
|
35
|
-
</div>
|
|
36
|
-
<p className="body muted">
|
|
37
|
-
When mounted by the shell, this card renders the Note you opened.
|
|
38
|
-
</p>
|
|
39
|
-
</>
|
|
40
|
-
)}
|
|
20
|
+
import { resolveView, useShell, type ViewRoutes } from '@/shell'
|
|
21
|
+
import { MonitorView } from '@/views/monitor'
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
23
|
+
const ROUTES: ViewRoutes = {
|
|
24
|
+
'/ui/monitor': MonitorView,
|
|
46
25
|
}
|
|
47
26
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* handshake is `ready`, so `session` is non-null here).
|
|
52
|
-
*/
|
|
53
|
-
function NoteCard({ session, nodeId }: { session: NodeSession; nodeId: string | undefined }) {
|
|
54
|
-
const state = useNode(session, nodeId)
|
|
55
|
-
|
|
56
|
-
const title =
|
|
57
|
-
state.status === 'ok' ? (readProp(state.node.props, PROP.named.name) ?? 'Note') : 'Note'
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<>
|
|
61
|
-
<h1 className="title">{title}</h1>
|
|
62
|
-
<p className="subline">
|
|
63
|
-
{state.status === 'ok'
|
|
64
|
-
? `${classShortName(state.node)} · ${state.node.path}`
|
|
65
|
-
: 'ui-note · Astrale view SPA'}
|
|
66
|
-
</p>
|
|
67
|
-
|
|
68
|
-
{state.status === 'idle' && (
|
|
69
|
-
<p className="body muted">Handshake complete, but no target node / token yet.</p>
|
|
70
|
-
)}
|
|
71
|
-
|
|
72
|
-
{state.status === 'loading' && <p className="muted">Loading the Note…</p>}
|
|
27
|
+
export function App() {
|
|
28
|
+
const shell = useShell()
|
|
29
|
+
const View = resolveView(ROUTES)
|
|
73
30
|
|
|
74
|
-
|
|
75
|
-
<div className="banner">Failed to load the Note: {state.message}</div>
|
|
76
|
-
)}
|
|
31
|
+
if (!View) return <UnknownView />
|
|
77
32
|
|
|
78
|
-
|
|
79
|
-
</>
|
|
80
|
-
)
|
|
33
|
+
return <>{View(shell)}</>
|
|
81
34
|
}
|
|
82
35
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
36
|
+
/** Fallback when the iframe's mount path has no registered view. */
|
|
37
|
+
function UnknownView() {
|
|
38
|
+
return (
|
|
39
|
+
<div className="wrap">
|
|
40
|
+
<div className="card">
|
|
41
|
+
<h1 className="title">No view here</h1>
|
|
42
|
+
<p className="subline">Astrale view SPA</p>
|
|
43
|
+
<div className="banner">
|
|
44
|
+
No view registered for {window.location.pathname}. Add a ROUTES entry in{' '}
|
|
45
|
+
<code>src/app.tsx</code> (and a matching <code>defineView({'{ mount }'})</code> in the
|
|
46
|
+
domain's <code>views/</code>).
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
94
51
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRoot } from 'react-dom/client'
|
|
2
2
|
|
|
3
3
|
import './styles.css'
|
|
4
|
-
import { App } from '
|
|
4
|
+
import { App } from '@/app'
|
|
5
5
|
|
|
6
6
|
const root = document.getElementById('root')
|
|
7
|
-
if (!root) throw new Error('
|
|
7
|
+
if (!root) throw new Error('astrale view client: #root missing from index.html')
|
|
8
8
|
|
|
9
9
|
createRoot(root).render(<App />)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonitorCard — the Monitor feature's one container. It owns the data/logic:
|
|
3
|
+
* loads the node (`useMonitor`), wires the `check` write (`useCheck`), and
|
|
4
|
+
* composes the presentation. Handles the query's loading/error/ok states; on
|
|
5
|
+
* `ok` it renders a `Panel` whose body is the check-error banner (if any) plus
|
|
6
|
+
* the pure `MonitorDetails` view, with a "Check now" button that runs the probe
|
|
7
|
+
* then reloads the node so the fresh values render.
|
|
8
|
+
*
|
|
9
|
+
* Thin by design: the record's layout lives in `monitor/ui` (`MonitorDetails`);
|
|
10
|
+
* the generic surfaces come from the `@/ui` design system.
|
|
11
|
+
*/
|
|
12
|
+
import type { KernelClient } from '@/shell'
|
|
13
|
+
|
|
14
|
+
import { ErrorBanner, Panel, Spinner } from '@/ui'
|
|
15
|
+
|
|
16
|
+
import { useCheck, useMonitor } from '../hooks'
|
|
17
|
+
import { MonitorDetails } from '../ui'
|
|
18
|
+
|
|
19
|
+
export function MonitorCard({ session, nodeId }: { session: KernelClient; nodeId: string }) {
|
|
20
|
+
const monitor = useMonitor(session, nodeId)
|
|
21
|
+
const probe = useCheck(session, nodeId)
|
|
22
|
+
|
|
23
|
+
async function checkNow() {
|
|
24
|
+
if (await probe.run()) monitor.reload()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (monitor.state === 'idle' || monitor.state === 'loading') {
|
|
28
|
+
return <Spinner label="Loading the Monitor…" />
|
|
29
|
+
}
|
|
30
|
+
if (monitor.state === 'error') {
|
|
31
|
+
return <ErrorBanner>Failed to load the Monitor: {monitor.message}</ErrorBanner>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const record = monitor.record!
|
|
35
|
+
const checking = probe.phase === 'running'
|
|
36
|
+
const checkButton = (
|
|
37
|
+
<button type="button" className="check-btn" onClick={checkNow} disabled={checking}>
|
|
38
|
+
{checking ? 'Checking…' : 'Check now'}
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Panel title={record.name} actions={checkButton}>
|
|
44
|
+
{probe.phase === 'failed' && probe.error && (
|
|
45
|
+
<ErrorBanner>Check failed: {probe.error}</ErrorBanner>
|
|
46
|
+
)}
|
|
47
|
+
<MonitorDetails record={record} />
|
|
48
|
+
</Panel>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MonitorCard } from './MonitorCard'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Run a Monitor's `check` probe — the feature's one write capability. */
|
|
2
|
+
import { type Capability, type KernelClient, useCapability } from '@/shell'
|
|
3
|
+
|
|
4
|
+
import { check } from '../monitor.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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Load a Monitor node → typed record, with a `reload()` for post-check refresh. */
|
|
2
|
+
import { useCallback, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { type KernelClient, useNode } from '@/shell'
|
|
5
|
+
|
|
6
|
+
import type { MonitorRecord } from '../monitor.types'
|
|
7
|
+
|
|
8
|
+
import { monitorFromNode } from '../monitor.mappers'
|
|
9
|
+
|
|
10
|
+
export type MonitorQuery = {
|
|
11
|
+
/** Lifecycle of the underlying `@<id>::get`. */
|
|
12
|
+
state: 'idle' | 'loading' | 'error' | 'ok'
|
|
13
|
+
/** The projected record once `ok`. */
|
|
14
|
+
record?: MonitorRecord
|
|
15
|
+
/** Failure message once `error`. */
|
|
16
|
+
message?: string
|
|
17
|
+
/** Re-fetch the node — call after `check` mutates it server-side. */
|
|
18
|
+
reload(): void
|
|
19
|
+
/** True while a `reload()`-triggered re-fetch is in flight. */
|
|
20
|
+
reloading: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load the Monitor node `nodeId` and project it to a `MonitorRecord`. 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.
|
|
29
|
+
*/
|
|
30
|
+
export function useMonitor(session: KernelClient, nodeId: string): MonitorQuery {
|
|
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])
|
|
53
|
+
|
|
54
|
+
switch (node.status) {
|
|
55
|
+
case 'ok':
|
|
56
|
+
return { state: 'ok', record: monitorFromNode(node.node), reload, reloading }
|
|
57
|
+
case 'error':
|
|
58
|
+
return { state: 'error', message: node.message, reload, reloading }
|
|
59
|
+
case 'loading':
|
|
60
|
+
return { state: 'loading', reload, reloading }
|
|
61
|
+
default:
|
|
62
|
+
return { state: 'idle', reload, reloading }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The Monitor feature — its api, types, mappers, hooks and components. */
|
|
2
|
+
export * from './components'
|
|
3
|
+
export * from './hooks'
|
|
4
|
+
export { check } from './monitor.api'
|
|
5
|
+
export { monitorFromNode, normalizeStatus } from './monitor.mappers'
|
|
6
|
+
export type { MonitorRecord } from './monitor.types'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Raw kernel calls for the Monitor feature — node instance methods. */
|
|
2
|
+
import { invokeNode, type KernelClient } from '@/shell'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run the Monitor's `check` instance method (`@<id>::check`). Probes the target
|
|
6
|
+
* URL server-side and updates the node's `status`/`statusCode`/`latencyMs`/
|
|
7
|
+
* `lastCheckedAt` props; the caller reloads the node to render the fresh values.
|
|
8
|
+
*/
|
|
9
|
+
export function check(session: KernelClient, nodeId: string): Promise<unknown> {
|
|
10
|
+
return invokeNode(session, nodeId, 'check', {})
|
|
11
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Node → typed record transforms for the Monitor feature. */
|
|
2
|
+
import { type KernelNode, PROP, readProp, readPropBySuffix } from '@/shell'
|
|
3
|
+
|
|
4
|
+
import type { MonitorRecord, MonitorStatus } from './monitor.types'
|
|
5
|
+
|
|
6
|
+
const lastSegment = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
|
|
7
|
+
|
|
8
|
+
/** Coerce a string prop to a finite number, or `undefined`. */
|
|
9
|
+
function readNumberBySuffix(props: Record<string, unknown>, suffix: string): number | undefined {
|
|
10
|
+
const raw = readPropBySuffix(props, suffix)
|
|
11
|
+
if (raw === undefined) return undefined
|
|
12
|
+
const n = Number(raw)
|
|
13
|
+
return Number.isFinite(n) ? n : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Normalize the node's `status` prop to the three known states. */
|
|
17
|
+
export function normalizeStatus(raw: string | undefined): MonitorStatus {
|
|
18
|
+
return raw === 'up' || raw === 'down' ? raw : 'unknown'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Project a Monitor `KernelNode` into a `MonitorRecord`. The name comes from the
|
|
23
|
+
* kernel `Named.name` key; the domain props (`url`/`status`/`statusCode`/
|
|
24
|
+
* `latencyMs`/`lastCheckedAt`) are domain-qualified, so read them by suffix.
|
|
25
|
+
*/
|
|
26
|
+
export function monitorFromNode(node: KernelNode): MonitorRecord {
|
|
27
|
+
const p = node.props ?? {}
|
|
28
|
+
return {
|
|
29
|
+
id: node.id,
|
|
30
|
+
path: node.path ?? '',
|
|
31
|
+
name: readProp(p, PROP.named.name) ?? lastSegment(node.path ?? '') ?? node.id,
|
|
32
|
+
url: readPropBySuffix(p, '.property.url') ?? '',
|
|
33
|
+
status: normalizeStatus(readPropBySuffix(p, '.property.status')),
|
|
34
|
+
statusCode: readNumberBySuffix(p, '.property.statusCode'),
|
|
35
|
+
latencyMs: readNumberBySuffix(p, '.property.latencyMs'),
|
|
36
|
+
lastCheckedAt: readPropBySuffix(p, '.property.lastCheckedAt'),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The Monitor feature's client types. */
|
|
2
|
+
|
|
3
|
+
/** The three known states of a Monitor's normalized status. */
|
|
4
|
+
export type MonitorStatus = 'up' | 'down' | 'unknown'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client-side projection of a Monitor node. Domain props are read off the
|
|
8
|
+
* kernel node by suffix (see `monitor.mappers.ts`); numeric props are coerced.
|
|
9
|
+
*/
|
|
10
|
+
export type MonitorRecord = {
|
|
11
|
+
id: string
|
|
12
|
+
path: string
|
|
13
|
+
name: string
|
|
14
|
+
url: string
|
|
15
|
+
/** up | down | unknown — normalized from the node's `status` prop. */
|
|
16
|
+
status: MonitorStatus
|
|
17
|
+
/** HTTP status code from the last check, if any. */
|
|
18
|
+
statusCode?: number
|
|
19
|
+
/** Round-trip latency of the last check, in milliseconds. */
|
|
20
|
+
latencyMs?: number
|
|
21
|
+
/** ISO timestamp of the last check, if any. */
|
|
22
|
+
lastCheckedAt?: string
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonitorDetails — the Monitor record's PRESENTATION, extracted pure. Given a
|
|
3
|
+
* projected `MonitorRecord`, it lays out the StatusBadge + the url/latency/
|
|
4
|
+
* statusCode/last-checked KV rows using the feature-agnostic `@/ui` primitives.
|
|
5
|
+
*
|
|
6
|
+
* Pure: no hooks, no kernel, no data loading — props in, DOM out. The container
|
|
7
|
+
* (`MonitorCard`) owns loading the record and wiring the "Check now" write.
|
|
8
|
+
*/
|
|
9
|
+
import { ExternalLink, KV, Mono, relativeTime } from '@/ui'
|
|
10
|
+
|
|
11
|
+
import type { MonitorRecord } from '../monitor.types'
|
|
12
|
+
|
|
13
|
+
import { StatusBadge } from './StatusBadge.UI'
|
|
14
|
+
|
|
15
|
+
export function MonitorDetails({ record }: { record: MonitorRecord }) {
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<div className="status-row">
|
|
19
|
+
<StatusBadge status={record.status} />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div className="kv">
|
|
23
|
+
<KV label="url">
|
|
24
|
+
<ExternalLink url={record.url || undefined} />
|
|
25
|
+
</KV>
|
|
26
|
+
<KV label="latency">
|
|
27
|
+
<Mono value={record.latencyMs !== undefined ? `${record.latencyMs}ms` : undefined} />
|
|
28
|
+
</KV>
|
|
29
|
+
<KV label="status code">
|
|
30
|
+
<Mono value={record.statusCode !== undefined ? String(record.statusCode) : undefined} />
|
|
31
|
+
</KV>
|
|
32
|
+
<KV label="last checked">
|
|
33
|
+
<Mono value={relativeTime(record.lastCheckedAt)} title={record.lastCheckedAt} />
|
|
34
|
+
</KV>
|
|
35
|
+
</div>
|
|
36
|
+
</>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Monitor status chip — pure presentational; styling lives in `styles.css`. */
|
|
2
|
+
|
|
3
|
+
import type { MonitorStatus } from '../monitor.types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Color-coded status pill: up = green, down = red, unknown = muted. The label is
|
|
7
|
+
* the uppercased status (`UP` / `DOWN` / `UNKNOWN`). Monitor-specific — it knows
|
|
8
|
+
* the feature's up/down/unknown vocabulary — so it lives in `monitor/ui`, not the
|
|
9
|
+
* feature-agnostic `@/ui` design system.
|
|
10
|
+
*/
|
|
11
|
+
export function StatusBadge({ status }: { status: MonitorStatus }) {
|
|
12
|
+
const label = status === 'up' ? 'UP' : status === 'down' ? 'DOWN' : 'UNKNOWN'
|
|
13
|
+
return <span className={`status-badge status-${status}`}>{label}</span>
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Monitor feature's OWN presentation — monitor-specific, feature-aware UI
|
|
3
|
+
* (it knows the up/down/unknown status vocabulary and the record shape). Pure
|
|
4
|
+
* components, built on the feature-agnostic `@/ui` primitives. Import within the
|
|
5
|
+
* feature: `import { StatusBadge, MonitorDetails } from '../ui'`.
|
|
6
|
+
*/
|
|
7
|
+
export { StatusBadge } from './StatusBadge.UI'
|
|
8
|
+
export { MonitorDetails } from './MonitorDetails.UI'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph/prop helpers for the nodes the kernel returns — reading fully-qualified
|
|
3
|
+
* props and short class names off a `KernelNode`. Pure shaping: the kernel
|
|
4
|
+
* TRANSPORT now lives in `@astrale-os/shell` (`shell.kernel` → `session.call`),
|
|
5
|
+
* so this file no longer speaks the wire.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prop key constants — the graph stores props with fully-qualified keys
|
|
10
|
+
* (`<domain>:<member>.property.<name>`). The kernel `Named.name` key is fixed
|
|
11
|
+
* and known; domain props (`url`, `status`) are qualified by the (build-time
|
|
12
|
+
* unknown) domain origin — read those by suffix with `readPropBySuffix`.
|
|
13
|
+
*/
|
|
14
|
+
export const PROP = {
|
|
15
|
+
named: {
|
|
16
|
+
name: 'kernel.astrale.ai:interface.Named.property.name',
|
|
17
|
+
},
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
export type KernelNode = {
|
|
21
|
+
id: string
|
|
22
|
+
path: string
|
|
23
|
+
class: string | { raw?: string }
|
|
24
|
+
props: Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readProp(props: Record<string, unknown>, key: string): string | undefined {
|
|
28
|
+
const v = props[key]
|
|
29
|
+
return typeof v === 'string' ? v : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read a string prop by key suffix — for domain-qualified props whose full key
|
|
34
|
+
* embeds the (build-time-unknown) domain origin. e.g.
|
|
35
|
+
* `readPropBySuffix(props, '.property.url')`.
|
|
36
|
+
*/
|
|
37
|
+
export function readPropBySuffix(
|
|
38
|
+
props: Record<string, unknown>,
|
|
39
|
+
suffix: string,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
for (const [k, v] of Object.entries(props)) {
|
|
42
|
+
if (k.endsWith(suffix) && typeof v === 'string') return v
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Short class name from a `class.raw` path `/:<domain>:class.<Name>` → `<Name>`. */
|
|
48
|
+
export function classShortName(node: KernelNode): string {
|
|
49
|
+
const raw = (typeof node.class === 'string' ? node.class : node.class?.raw) ?? ''
|
|
50
|
+
const last = raw.split(':').pop() ?? ''
|
|
51
|
+
const dot = last.indexOf('.')
|
|
52
|
+
return dot >= 0 ? last.slice(dot + 1) : last
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Operator-facing message for a failed kernel call. Errors from the shell's
|
|
57
|
+
* kernel client carry a numeric `code` (e.g. `NotFoundError` → `code 3002`) with
|
|
58
|
+
* the raw server text in `message`; surface them as `<code>: <message>` so the
|
|
59
|
+
* code stays visible. Plain errors fall back to their message.
|
|
60
|
+
*/
|
|
61
|
+
export function errorMessage(err: unknown): string {
|
|
62
|
+
if (err instanceof Error) {
|
|
63
|
+
const code = (err as { code?: unknown }).code
|
|
64
|
+
return typeof code === 'number' ? `${code}: ${err.message}` : err.message
|
|
65
|
+
}
|
|
66
|
+
return String(err)
|
|
67
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kernel boundary — everything for talking to the kernel and the GUI
|
|
3
|
+
* shell, plus the common read/write primitives features build their
|
|
4
|
+
* `.query`/`.mutation` hooks on. Import from the barrel: `@/shell`.
|
|
5
|
+
*/
|
|
6
|
+
export { classShortName, errorMessage, PROP, readProp, readPropBySuffix } from './client'
|
|
7
|
+
export type { KernelNode } from './client'
|
|
8
|
+
export { callMethod, invokeNode, nodeAddr } from './invoke'
|
|
9
|
+
export type { KernelClient } from '@astrale-os/shell'
|
|
10
|
+
export { useNode } from './use-node'
|
|
11
|
+
export type { NodeState } from './use-node'
|
|
12
|
+
export { useShell } from './use-shell'
|
|
13
|
+
export type { ShellState, ShellStatus } from './use-shell'
|
|
14
|
+
export { useAsync } from './use-async'
|
|
15
|
+
export type { AsyncResource, AsyncState } from './use-async'
|
|
16
|
+
export { useCapability } from './use-capability'
|
|
17
|
+
export type { Capability, Phase } from './use-capability'
|
|
18
|
+
export { asNodeArray, linkTargets, qualifiedProp, qualifiedString } from './transformers'
|
|
19
|
+
export { resolveView, ViewFrame } from './view-router'
|
|
20
|
+
export type { ViewComponent, ViewRoutes } from './view-router'
|