@marcusrbrown/infra 0.11.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__snapshots__/cli.test.ts.snap +19 -1
- package/src/cli.ts +2 -0
- package/src/commands/dashboard/deploy.test.ts +218 -0
- package/src/commands/dashboard/deploy.ts +135 -0
- package/src/commands/dashboard/host.test.ts +79 -0
- package/src/commands/dashboard/host.ts +33 -0
- package/src/commands/dashboard/index.ts +13 -0
- package/src/commands/dashboard/logs.test.ts +293 -0
- package/src/commands/dashboard/logs.ts +178 -0
- package/src/commands/dashboard/status.test.ts +652 -0
- package/src/commands/dashboard/status.ts +290 -0
- package/src/commands/mcp.test.ts +5 -0
- package/src/commands/mcp.ts +3 -0
- package/src/commands/status.test.ts +17 -0
- package/src/commands/status.ts +12 -3
- package/src/resources/known_hosts +8 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type {goke} from 'goke'
|
|
2
|
+
import type {ActionCtx} from '../../lib/action-ctx'
|
|
3
|
+
import type {StatusSummary} from '../status'
|
|
4
|
+
import {z} from 'zod'
|
|
5
|
+
|
|
6
|
+
import {buildKnownHostsArgs} from '../../lib/known-hosts'
|
|
7
|
+
import {redactHost} from '../../lib/redact'
|
|
8
|
+
import {buildIdentityArgs} from '../../lib/ssh-identity'
|
|
9
|
+
import {validateDashboardHost} from './host'
|
|
10
|
+
|
|
11
|
+
declare const process: {
|
|
12
|
+
env: Record<string, string | undefined>
|
|
13
|
+
exitCode?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const COMPOSE_PROJECT_DIR = '/opt/dashboard'
|
|
17
|
+
|
|
18
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface ComposePsEntry {
|
|
21
|
+
Name: string
|
|
22
|
+
State: string
|
|
23
|
+
Health: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HealthStatus = 'healthy' | 'unhealthy' | 'starting' | 'n-a'
|
|
27
|
+
|
|
28
|
+
export interface ServiceRow {
|
|
29
|
+
service: string
|
|
30
|
+
state: string
|
|
31
|
+
health: HealthStatus
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DashboardStatusResult {
|
|
35
|
+
ok: boolean
|
|
36
|
+
services: ServiceRow[]
|
|
37
|
+
error?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type SpawnFn = (
|
|
41
|
+
cmd: string[],
|
|
42
|
+
opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
|
|
43
|
+
) => {
|
|
44
|
+
stdout: ReadableStream<Uint8Array>
|
|
45
|
+
stderr: ReadableStream<Uint8Array>
|
|
46
|
+
exited: Promise<number>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Pure helpers ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function parseComposePsOutput(stdoutText: string): ComposePsEntry[] {
|
|
52
|
+
const trimmed = stdoutText.trim()
|
|
53
|
+
if (trimmed.length === 0) return []
|
|
54
|
+
|
|
55
|
+
// Legacy compose may emit a single JSON array; current versions emit NDJSON.
|
|
56
|
+
if (trimmed.startsWith('[')) {
|
|
57
|
+
const parsed: unknown = JSON.parse(trimmed)
|
|
58
|
+
return Array.isArray(parsed) ? (parsed as ComposePsEntry[]) : []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// NDJSON: one JSON object per non-empty line.
|
|
62
|
+
return trimmed
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map(line => line.trim())
|
|
65
|
+
.filter(line => line.length > 0)
|
|
66
|
+
.map(line => JSON.parse(line) as ComposePsEntry)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeHealth(raw: string): HealthStatus {
|
|
70
|
+
if (raw === 'healthy' || raw === 'unhealthy' || raw === 'starting') {
|
|
71
|
+
return raw
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 'n-a'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseComposePs(entries: ComposePsEntry[]): ServiceRow[] {
|
|
78
|
+
return entries.map(entry => ({
|
|
79
|
+
service: entry.Name,
|
|
80
|
+
state: entry.State,
|
|
81
|
+
health: normalizeHealth(entry.Health),
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isAllRunning(rows: ServiceRow[]): boolean {
|
|
86
|
+
return rows.every(row => row.state === 'running' && (row.health === 'healthy' || row.health === 'n-a'))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── SSH-backed status fetch ──────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function defaultSpawn(
|
|
92
|
+
cmd: string[],
|
|
93
|
+
opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
|
|
94
|
+
): ReturnType<SpawnFn> {
|
|
95
|
+
return Bun.spawn(cmd, opts) as ReturnType<SpawnFn>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getDashboardComposeStatus(
|
|
99
|
+
host: string,
|
|
100
|
+
spawn: SpawnFn = defaultSpawn,
|
|
101
|
+
): Promise<DashboardStatusResult> {
|
|
102
|
+
validateDashboardHost(host)
|
|
103
|
+
|
|
104
|
+
const knownHostsArgs = buildKnownHostsArgs()
|
|
105
|
+
const {args: identityArgs, cleanup} = buildIdentityArgs(process.env.DASHBOARD_SSH_KEY)
|
|
106
|
+
|
|
107
|
+
const sshCmd = [
|
|
108
|
+
'ssh',
|
|
109
|
+
'-o',
|
|
110
|
+
'BatchMode=yes',
|
|
111
|
+
'-o',
|
|
112
|
+
'StrictHostKeyChecking=yes',
|
|
113
|
+
...knownHostsArgs,
|
|
114
|
+
...identityArgs,
|
|
115
|
+
`root@${host}`,
|
|
116
|
+
`docker compose --project-directory ${COMPOSE_PROJECT_DIR} ps --format json`,
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
const env: Record<string, string> = {
|
|
120
|
+
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
121
|
+
HOME: process.env.HOME ?? '/tmp',
|
|
122
|
+
...(process.env.SSH_AUTH_SOCK ? {SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK} : {}),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let stdoutText: string
|
|
126
|
+
let stderrText: string
|
|
127
|
+
let exitCode: number
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const child = spawn(sshCmd, {env, stdout: 'pipe', stderr: 'pipe'})
|
|
131
|
+
;[stdoutText, stderrText, exitCode] = await Promise.all([
|
|
132
|
+
new Response(child.stdout).text(),
|
|
133
|
+
new Response(child.stderr).text(),
|
|
134
|
+
child.exited,
|
|
135
|
+
])
|
|
136
|
+
} finally {
|
|
137
|
+
cleanup()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (exitCode !== 0) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
services: [],
|
|
144
|
+
error: `SSH command failed (exit ${exitCode}): ${redactHost(stderrText.trim(), host) || 'unknown error'}`,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let entries: ComposePsEntry[]
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
entries = parseComposePsOutput(stdoutText)
|
|
152
|
+
} catch {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
services: [],
|
|
156
|
+
error: `Failed to parse docker compose ps output: ${redactHost(stdoutText.slice(0, 200), host)}`,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const services = parseComposePs(entries)
|
|
161
|
+
const ok = isAllRunning(services)
|
|
162
|
+
|
|
163
|
+
return {ok, services}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Unified status aggregator export ────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export async function getDashboardStatusSummary(host: string, spawn?: SpawnFn): Promise<StatusSummary> {
|
|
169
|
+
let result: DashboardStatusResult
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
result = await getDashboardComposeStatus(host, spawn)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const errorMsg = error instanceof Error ? error.message : String(error)
|
|
175
|
+
return {
|
|
176
|
+
app: 'dashboard',
|
|
177
|
+
http: `ERROR: ${errorMsg}`,
|
|
178
|
+
lastDeploy: '—',
|
|
179
|
+
version: '—',
|
|
180
|
+
contentHash: '—',
|
|
181
|
+
usageStats: '—',
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Three-way split: no services → ERROR, services present but degraded → DEGRADED, all healthy → OK
|
|
186
|
+
if (result.services.length === 0) {
|
|
187
|
+
const errorMsg = result.error ?? 'No services reported'
|
|
188
|
+
return {
|
|
189
|
+
app: 'dashboard',
|
|
190
|
+
http: `ERROR: ${errorMsg}`,
|
|
191
|
+
lastDeploy: '—',
|
|
192
|
+
version: '—',
|
|
193
|
+
contentHash: '—',
|
|
194
|
+
usageStats: '—',
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rows = result.services.map(s => `${s.service}:${s.state}/${s.health}`).join(', ')
|
|
199
|
+
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
return {
|
|
202
|
+
app: 'dashboard',
|
|
203
|
+
http: `DEGRADED: ${rows}`,
|
|
204
|
+
lastDeploy: '—',
|
|
205
|
+
version: '—',
|
|
206
|
+
contentHash: '—',
|
|
207
|
+
usageStats: '—',
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
app: 'dashboard',
|
|
213
|
+
http: `OK: ${rows}`,
|
|
214
|
+
lastDeploy: '—',
|
|
215
|
+
version: '—',
|
|
216
|
+
contentHash: '—',
|
|
217
|
+
usageStats: '—',
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Action (exported for direct testing) ────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
export async function dashboardStatusAction(
|
|
224
|
+
options: {key?: string | undefined},
|
|
225
|
+
ctx: ActionCtx,
|
|
226
|
+
spawn?: SpawnFn,
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
const hostEnvKey = options.key ?? 'DASHBOARD_DOMAIN'
|
|
229
|
+
const host = process.env[hostEnvKey]
|
|
230
|
+
|
|
231
|
+
if (!host) {
|
|
232
|
+
ctx.console.error(
|
|
233
|
+
'Dashboard host not set. Export DASHBOARD_DOMAIN or pass --key <env-name> pointing to a set variable.',
|
|
234
|
+
)
|
|
235
|
+
ctx.process.exit(1)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
ctx.console.log('Dashboard status')
|
|
240
|
+
ctx.console.log('')
|
|
241
|
+
|
|
242
|
+
let result: DashboardStatusResult
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
result = await getDashboardComposeStatus(host, spawn)
|
|
246
|
+
} catch (error) {
|
|
247
|
+
ctx.console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
248
|
+
ctx.process.exit(1)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (result.services.length === 0) {
|
|
253
|
+
ctx.console.error(`Error: No services reported by docker compose ps`)
|
|
254
|
+
ctx.process.exit(1)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
ctx.console.log('Service State Health')
|
|
259
|
+
ctx.console.log('─────────────────────────────────────')
|
|
260
|
+
|
|
261
|
+
for (const row of result.services) {
|
|
262
|
+
const svc = row.service.padEnd(16)
|
|
263
|
+
const state = row.state.padEnd(10)
|
|
264
|
+
ctx.console.log(`${svc} ${state} ${row.health}`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
ctx.console.log('')
|
|
268
|
+
|
|
269
|
+
if (result.ok) {
|
|
270
|
+
ctx.console.log('Status: OK')
|
|
271
|
+
} else {
|
|
272
|
+
ctx.console.log('Status: DEGRADED (one or more services not running)')
|
|
273
|
+
ctx.process.exit(1)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Command registration ─────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
export function registerDashboardStatus(cli: ReturnType<typeof goke>): void {
|
|
280
|
+
cli
|
|
281
|
+
.command('dashboard status', 'Show operational health of the dashboard deployment via docker compose ps.')
|
|
282
|
+
.option(
|
|
283
|
+
'--key [key]',
|
|
284
|
+
z
|
|
285
|
+
.string()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe('Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.'),
|
|
288
|
+
)
|
|
289
|
+
.action((options, ctx) => dashboardStatusAction(options, ctx))
|
|
290
|
+
}
|
package/src/commands/mcp.test.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from
|
|
|
30
30
|
import {goke} from 'goke'
|
|
31
31
|
|
|
32
32
|
import {registerCliproxyCommands} from './cliproxy'
|
|
33
|
+
import {registerDashboardCommands} from './dashboard'
|
|
33
34
|
import {registerGatewayCommands} from './gateway'
|
|
34
35
|
import {registerKeewebCommands} from './keeweb'
|
|
35
36
|
import {MCP_ALLOWLIST, registerMcp} from './mcp'
|
|
@@ -47,6 +48,7 @@ import {registerVpnCommands} from './vpn'
|
|
|
47
48
|
const EXPECTED_TOOLS = [
|
|
48
49
|
'cliproxy_models',
|
|
49
50
|
'cliproxy_status',
|
|
51
|
+
'dashboard_status',
|
|
50
52
|
'gateway_status',
|
|
51
53
|
'keeweb_status',
|
|
52
54
|
'status',
|
|
@@ -65,6 +67,8 @@ const CLI_ONLY_TOOLS = [
|
|
|
65
67
|
'cliproxy_login',
|
|
66
68
|
'cliproxy_open',
|
|
67
69
|
'cliproxy_setup',
|
|
70
|
+
'dashboard_deploy',
|
|
71
|
+
'dashboard_logs',
|
|
68
72
|
'gateway_deploy',
|
|
69
73
|
'gateway_logs',
|
|
70
74
|
'gateway_restore',
|
|
@@ -96,6 +100,7 @@ function buildTestCli(): ReturnType<typeof goke> {
|
|
|
96
100
|
registerCliproxyCommands(cli)
|
|
97
101
|
registerGatewayCommands(cli)
|
|
98
102
|
registerUmamiCommands(cli)
|
|
103
|
+
registerDashboardCommands(cli)
|
|
99
104
|
registerVpnCommands(cli)
|
|
100
105
|
registerStatus(cli)
|
|
101
106
|
registerMcp(cli)
|
package/src/commands/mcp.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {createMcpAction} from '@goke/mcp'
|
|
|
16
16
|
* - `keeweb open` — host-machine side effect (spawns local browser, requires user intent)
|
|
17
17
|
* - `umami deploy` — intentionally CLI-only: mutates live deployment and requires environment approval
|
|
18
18
|
* - `umami logs` — intentionally CLI-only: streams logs that may emit sensitive data (DB passwords, app secrets)
|
|
19
|
+
* - `dashboard deploy` — intentionally CLI-only: mutates live deployment and requires environment approval
|
|
20
|
+
* - `dashboard logs` — intentionally CLI-only: streams logs that may emit sensitive data (DB passwords, app secrets)
|
|
19
21
|
*
|
|
20
22
|
* Source-gated sensitive commands (security — CLI-only, never MCP-exposed):
|
|
21
23
|
* - `gateway backup` — secret-bearing: writes CA private key material to a tarball; CLI-only
|
|
@@ -41,6 +43,7 @@ export const MCP_ALLOWLIST: ReadonlySet<string> = new Set([
|
|
|
41
43
|
'cliproxy models',
|
|
42
44
|
'keeweb status',
|
|
43
45
|
'umami status',
|
|
46
|
+
'dashboard status',
|
|
44
47
|
'vpn status',
|
|
45
48
|
'status',
|
|
46
49
|
])
|
|
@@ -4,6 +4,8 @@ import {goke} from 'goke'
|
|
|
4
4
|
import {createCapturedCtx, expectCapturedToInclude} from '../__test__/mcp-ctx-fixture'
|
|
5
5
|
import {registerStatus, unifiedStatusAction} from './status'
|
|
6
6
|
|
|
7
|
+
// NOTE: dashboard is included in the unified status table alongside the other apps.
|
|
8
|
+
|
|
7
9
|
const healthyKeeweb: StatusSummary = {
|
|
8
10
|
app: 'keeweb',
|
|
9
11
|
http: 'OK',
|
|
@@ -49,12 +51,22 @@ const healthyVpn: StatusSummary = {
|
|
|
49
51
|
usageStats: '—',
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
const healthyDashboard: StatusSummary = {
|
|
55
|
+
app: 'dashboard',
|
|
56
|
+
http: 'OK: dashboard:running/healthy',
|
|
57
|
+
lastDeploy: '—',
|
|
58
|
+
version: '—',
|
|
59
|
+
contentHash: '—',
|
|
60
|
+
usageStats: '—',
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
function makeDeps(overrides?: Partial<Parameters<typeof registerStatus>[1]>): Parameters<typeof registerStatus>[1] {
|
|
53
64
|
return {
|
|
54
65
|
getKeewebStatusSummary: async () => healthyKeeweb,
|
|
55
66
|
getCliproxyStatusSummary: async () => healthyCliproxy,
|
|
56
67
|
getGatewayStatusSummary: async () => healthyGateway,
|
|
57
68
|
getUmamiStatusSummary: async () => healthyUmami,
|
|
69
|
+
getDashboardStatusSummary: async () => healthyDashboard,
|
|
58
70
|
getVpnStatusSummary: async () => healthyVpn,
|
|
59
71
|
...overrides,
|
|
60
72
|
}
|
|
@@ -73,6 +85,9 @@ describe('top-level status command (Tier-2 ctx capture)', () => {
|
|
|
73
85
|
expect(expectCapturedToInclude(captured, '| cliproxy | OK | — | v1.2.3 | — | 12 req / 0 fail |')).toBe(true)
|
|
74
86
|
expect(expectCapturedToInclude(captured, '| gateway | OK: gateway:running/healthy | — | — | — | — |')).toBe(true)
|
|
75
87
|
expect(expectCapturedToInclude(captured, '| umami | OK: umami:running/healthy | — | — | — | — |')).toBe(true)
|
|
88
|
+
expect(expectCapturedToInclude(captured, '| dashboard | OK: dashboard:running/healthy | — | — | — | — |')).toBe(
|
|
89
|
+
true,
|
|
90
|
+
)
|
|
76
91
|
expect(expectCapturedToInclude(captured, '| vpn | OK: wg0 up, pubkey:TESTKEY== 1 peer(s) | — | — | — | — |')).toBe(
|
|
77
92
|
true,
|
|
78
93
|
)
|
|
@@ -171,6 +186,7 @@ describe('top-level status command (Tier-2 ctx capture)', () => {
|
|
|
171
186
|
cliproxy: {version: string}
|
|
172
187
|
gateway: {http: string}
|
|
173
188
|
umami: {http: string}
|
|
189
|
+
dashboard: {http: string}
|
|
174
190
|
vpn: {http: string}
|
|
175
191
|
}
|
|
176
192
|
|
|
@@ -178,6 +194,7 @@ describe('top-level status command (Tier-2 ctx capture)', () => {
|
|
|
178
194
|
expect(parsed.cliproxy.version).toBe('v1.2.3')
|
|
179
195
|
expect(parsed.gateway.http).toBe('OK: gateway:running/healthy')
|
|
180
196
|
expect(parsed.umami.http).toBe('OK: umami:running/healthy')
|
|
197
|
+
expect(parsed.dashboard.http).toBe('OK: dashboard:running/healthy')
|
|
181
198
|
expect(parsed.vpn.http).toBe('OK: wg0 up, pubkey:TESTKEY== 1 peer(s)')
|
|
182
199
|
})
|
|
183
200
|
|
package/src/commands/status.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {ActionCtx} from '../lib/action-ctx'
|
|
|
5
5
|
import {z} from 'zod'
|
|
6
6
|
|
|
7
7
|
import {getCliproxyStatusSummary} from './cliproxy/status'
|
|
8
|
+
import {getDashboardStatusSummary} from './dashboard'
|
|
8
9
|
import {getGatewayStatusSummary} from './gateway'
|
|
9
10
|
import {getKeewebStatusSummary} from './keeweb/status'
|
|
10
11
|
import {getUmamiStatusSummary} from './umami'
|
|
@@ -15,7 +16,7 @@ declare const process: {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface StatusSummary {
|
|
18
|
-
app: 'keeweb' | 'cliproxy' | 'gateway' | 'umami' | 'vpn'
|
|
19
|
+
app: 'keeweb' | 'cliproxy' | 'gateway' | 'umami' | 'dashboard' | 'vpn'
|
|
19
20
|
http: string
|
|
20
21
|
lastDeploy: string
|
|
21
22
|
version: string
|
|
@@ -30,6 +31,7 @@ interface StatusDependencies {
|
|
|
30
31
|
getCliproxyStatusSummary: (baseUrl: string, key: string, verbose: boolean) => Promise<StatusSummary>
|
|
31
32
|
getGatewayStatusSummary: (host: string) => Promise<StatusSummary>
|
|
32
33
|
getUmamiStatusSummary: (host: string) => Promise<StatusSummary>
|
|
34
|
+
getDashboardStatusSummary: (host: string) => Promise<StatusSummary>
|
|
33
35
|
getVpnStatusSummary: (host: string) => Promise<StatusSummary>
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -68,6 +70,7 @@ function toJsonPayload(rows: StatusSummary[]): Record<AppName, StatusSummary> {
|
|
|
68
70
|
cliproxy: rows.find(row => row.app === 'cliproxy') ?? errorSummary('cliproxy', 'missing result'),
|
|
69
71
|
gateway: rows.find(row => row.app === 'gateway') ?? errorSummary('gateway', 'missing result'),
|
|
70
72
|
umami: rows.find(row => row.app === 'umami') ?? errorSummary('umami', 'missing result'),
|
|
73
|
+
dashboard: rows.find(row => row.app === 'dashboard') ?? errorSummary('dashboard', 'missing result'),
|
|
71
74
|
vpn: rows.find(row => row.app === 'vpn') ?? errorSummary('vpn', 'missing result'),
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -85,6 +88,7 @@ export async function unifiedStatusAction(
|
|
|
85
88
|
getCliproxyStatusSummary,
|
|
86
89
|
getGatewayStatusSummary,
|
|
87
90
|
getUmamiStatusSummary,
|
|
91
|
+
getDashboardStatusSummary,
|
|
88
92
|
getVpnStatusSummary,
|
|
89
93
|
},
|
|
90
94
|
): Promise<void> {
|
|
@@ -93,6 +97,7 @@ export async function unifiedStatusAction(
|
|
|
93
97
|
const cliproxyKey = process.env.CLIPROXY_MANAGEMENT_KEY ?? ''
|
|
94
98
|
const gatewayHost = process.env.GATEWAY_HOST ?? ''
|
|
95
99
|
const umamiHost = process.env.UMAMI_DOMAIN ?? ''
|
|
100
|
+
const dashboardHost = process.env.DASHBOARD_DOMAIN ?? ''
|
|
96
101
|
const vpnHost = process.env.VPN_HOST ?? ''
|
|
97
102
|
|
|
98
103
|
const results = await Promise.allSettled([
|
|
@@ -100,10 +105,11 @@ export async function unifiedStatusAction(
|
|
|
100
105
|
dependencies.getCliproxyStatusSummary(cliproxyBaseUrl, cliproxyKey, verbose),
|
|
101
106
|
dependencies.getGatewayStatusSummary(gatewayHost),
|
|
102
107
|
dependencies.getUmamiStatusSummary(umamiHost),
|
|
108
|
+
dependencies.getDashboardStatusSummary(dashboardHost),
|
|
103
109
|
dependencies.getVpnStatusSummary(vpnHost),
|
|
104
110
|
])
|
|
105
111
|
|
|
106
|
-
const appNames: AppName[] = ['keeweb', 'cliproxy', 'gateway', 'umami', 'vpn']
|
|
112
|
+
const appNames: AppName[] = ['keeweb', 'cliproxy', 'gateway', 'umami', 'dashboard', 'vpn']
|
|
107
113
|
const rows: StatusSummary[] = results.map((result, index) => {
|
|
108
114
|
const app = appNames[index] ?? 'keeweb'
|
|
109
115
|
if (result.status === 'fulfilled') {
|
|
@@ -133,6 +139,7 @@ export function registerStatus(
|
|
|
133
139
|
getCliproxyStatusSummary,
|
|
134
140
|
getGatewayStatusSummary,
|
|
135
141
|
getUmamiStatusSummary,
|
|
142
|
+
getDashboardStatusSummary,
|
|
136
143
|
getVpnStatusSummary,
|
|
137
144
|
},
|
|
138
145
|
): void {
|
|
@@ -142,7 +149,9 @@ export function registerStatus(
|
|
|
142
149
|
'--json',
|
|
143
150
|
z
|
|
144
151
|
.boolean()
|
|
145
|
-
.describe(
|
|
152
|
+
.describe(
|
|
153
|
+
'Output machine-readable JSON with keeweb, cliproxy, gateway, umami, dashboard, and vpn summary objects.',
|
|
154
|
+
),
|
|
146
155
|
)
|
|
147
156
|
.option(
|
|
148
157
|
'--verbose',
|
|
@@ -44,3 +44,11 @@ metrics.fro.bot ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy
|
|
|
44
44
|
|1|6g8lttzHI3BCW6rIf1/Bteo1KcM=|bNbGP2QK2pUdPmhplw6eVzspe5g= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILA4LGgnvBjq/yoOKLjzFefGzo9gAspxwfbY8aACj/3q
|
|
45
45
|
# 52.208.116.13:22 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.15
|
|
46
46
|
# 52.208.116.13:22 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.15
|
|
47
|
+
|
|
48
|
+
# dashboard droplet (206.189.198.47 / dashboard.fro.bot)
|
|
49
|
+
dashboard.fro.bot ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDOO1TZlem4z3y9QPJ5GNYT+uwH1rkPQzBNC4GFM3pmFpQrikJSKNsB8m6ugvT30BChVPI2bOKmPUAfdstCQks2Creql9yYYkmo2PHnICNuj66saoOr6O+aACZYMJzjyLr3hcpxxFlLCYiu3fWkCoR8U+Zej3Fd+rjkSSm35jiAH/9y6pzLYO0HeIWGnDqg/qkTfQPwdGzlp+/PtWdClgECWO/dv2Op55cnoXBw9V3+DTvi9Dz8SqHs61InonZIkbGhleOF+JYzMinFkn9zcLTqZsnLhJwqF/xnhqjBGIpKUfL+cHAge5S2BVkudULQ93T/s7Ef6i0NyWNs3355D1XlaXPqIKQX7wZ95pfnAfdaMtHBNmb87YjzhFmyjjK+WewMDREPkYCmK5sP+CGgZTesFvEBM3R/3KbrHwqctFkZmjYVMhQ+AjYOCcXrqQNbqRbhYxKjQxJ4+JOIEmrvkO7/sN+2Ln/ieUeiNNwHQI3u2y3gh/86+b5hkkSfiM36Guk=
|
|
50
|
+
dashboard.fro.bot ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIbi0QIPYdxKt7NyZaQqRzqybeLaLWGpRz0HmJ/LlqCZvDyMKpgRPBbXdrX4VLJ7yjHMMPTE3c6AuESPaREAskQ=
|
|
51
|
+
dashboard.fro.bot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfh7XC2RI9+cjSQYilRUuTJ6LyYNemiqwpIrrtuGem4
|
|
52
|
+
|1|BE8EGlm+K9zcHciIkmp8myb/qaI=|Q8zNDhvS1r7zKIGzI54O/v9ev1I= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDOO1TZlem4z3y9QPJ5GNYT+uwH1rkPQzBNC4GFM3pmFpQrikJSKNsB8m6ugvT30BChVPI2bOKmPUAfdstCQks2Creql9yYYkmo2PHnICNuj66saoOr6O+aACZYMJzjyLr3hcpxxFlLCYiu3fWkCoR8U+Zej3Fd+rjkSSm35jiAH/9y6pzLYO0HeIWGnDqg/qkTfQPwdGzlp+/PtWdClgECWO/dv2Op55cnoXBw9V3+DTvi9Dz8SqHs61InonZIkbGhleOF+JYzMinFkn9zcLTqZsnLhJwqF/xnhqjBGIpKUfL+cHAge5S2BVkudULQ93T/s7Ef6i0NyWNs3355D1XlaXPqIKQX7wZ95pfnAfdaMtHBNmb87YjzhFmyjjK+WewMDREPkYCmK5sP+CGgZTesFvEBM3R/3KbrHwqctFkZmjYVMhQ+AjYOCcXrqQNbqRbhYxKjQxJ4+JOIEmrvkO7/sN+2Ln/ieUeiNNwHQI3u2y3gh/86+b5hkkSfiM36Guk=
|
|
53
|
+
|1|ecn8h6NS6U7O4Trw2TYxOU/NRBA=|reMYqISi8FjcMseG/m8hL2gZeh8= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIbi0QIPYdxKt7NyZaQqRzqybeLaLWGpRz0HmJ/LlqCZvDyMKpgRPBbXdrX4VLJ7yjHMMPTE3c6AuESPaREAskQ=
|
|
54
|
+
|1|MDYlzZB3WsZTegoHcaekI74yObo=|/l2M4GVvjP43mFxedVBB2aBkTQk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfh7XC2RI9+cjSQYilRUuTJ6LyYNemiqwpIrrtuGem4
|