@matthesketh/fleet 1.1.0 → 1.6.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/README.md +183 -251
- package/dist/adapters/detector/index.d.ts +8 -0
- package/dist/adapters/detector/index.js +54 -0
- package/dist/adapters/notifier/index.d.ts +2 -0
- package/dist/adapters/notifier/index.js +2 -0
- package/dist/adapters/notifier/stdout.d.ts +2 -0
- package/dist/adapters/notifier/stdout.js +8 -0
- package/dist/adapters/notifier/webhook.d.ts +9 -0
- package/dist/adapters/notifier/webhook.js +38 -0
- package/dist/adapters/runner/claude-cli.d.ts +7 -0
- package/dist/adapters/runner/claude-cli.js +231 -0
- package/dist/adapters/runner/mcp-call.d.ts +8 -0
- package/dist/adapters/runner/mcp-call.js +82 -0
- package/dist/adapters/runner/shell.d.ts +2 -0
- package/dist/adapters/runner/shell.js +103 -0
- package/dist/adapters/scheduler/systemd-timer.d.ts +17 -0
- package/dist/adapters/scheduler/systemd-timer.js +149 -0
- package/dist/adapters/signals/ci-status.d.ts +2 -0
- package/dist/adapters/signals/ci-status.js +79 -0
- package/dist/adapters/signals/container-up.d.ts +5 -0
- package/dist/adapters/signals/container-up.js +54 -0
- package/dist/adapters/signals/git-clean.d.ts +2 -0
- package/dist/adapters/signals/git-clean.js +55 -0
- package/dist/adapters/signals/index.d.ts +6 -0
- package/dist/adapters/signals/index.js +7 -0
- package/dist/adapters/types.d.ts +52 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli.js +43 -2
- package/dist/commands/add.js +0 -6
- package/dist/commands/boot-start.d.ts +1 -0
- package/dist/commands/boot-start.js +51 -0
- package/dist/commands/deploy.js +13 -0
- package/dist/commands/deps.js +5 -0
- package/dist/commands/egress.d.ts +1 -0
- package/dist/commands/egress.js +106 -0
- package/dist/commands/freeze.d.ts +4 -0
- package/dist/commands/freeze.js +64 -0
- package/dist/commands/logs.d.ts +1 -1
- package/dist/commands/logs.js +237 -8
- package/dist/commands/patch-systemd.d.ts +1 -0
- package/dist/commands/patch-systemd.js +126 -0
- package/dist/commands/rollback.d.ts +1 -0
- package/dist/commands/rollback.js +58 -0
- package/dist/commands/routine-run.d.ts +1 -0
- package/dist/commands/routine-run.js +122 -0
- package/dist/commands/routines.d.ts +1 -0
- package/dist/commands/routines.js +25 -0
- package/dist/commands/secrets.js +449 -16
- package/dist/commands/status.js +7 -3
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +16 -40
- package/dist/core/boot-refresh.d.ts +57 -0
- package/dist/core/boot-refresh.js +116 -0
- package/dist/core/deps/actors/pr-creator.js +11 -9
- package/dist/core/deps/collectors/docker-running.js +2 -2
- package/dist/core/deps/collectors/github-pr.js +5 -2
- package/dist/core/deps/collectors/npm.js +10 -5
- package/dist/core/deps/collectors/vulnerability.js +10 -6
- package/dist/core/deps/reporters/motd.js +1 -1
- package/dist/core/deps/reporters/telegram.js +2 -29
- package/dist/core/docker.js +45 -15
- package/dist/core/egress.d.ts +41 -0
- package/dist/core/egress.js +161 -0
- package/dist/core/exec.d.ts +7 -1
- package/dist/core/exec.js +25 -17
- package/dist/core/git.d.ts +1 -0
- package/dist/core/git.js +36 -23
- package/dist/core/github.js +27 -8
- package/dist/core/health.d.ts +3 -0
- package/dist/core/health.js +15 -3
- package/dist/core/logs-multi.d.ts +73 -0
- package/dist/core/logs-multi.js +163 -0
- package/dist/core/logs-policy.d.ts +55 -0
- package/dist/core/logs-policy.js +148 -0
- package/dist/core/nginx.js +8 -4
- package/dist/core/notify.d.ts +15 -0
- package/dist/core/notify.js +55 -0
- package/dist/core/registry.d.ts +25 -0
- package/dist/core/registry.js +57 -10
- package/dist/core/routines/cost-queries.d.ts +24 -0
- package/dist/core/routines/cost-queries.js +65 -0
- package/dist/core/routines/db.d.ts +9 -0
- package/dist/core/routines/db.js +126 -0
- package/dist/core/routines/defaults.d.ts +2 -0
- package/dist/core/routines/defaults.js +72 -0
- package/dist/core/routines/engine.d.ts +59 -0
- package/dist/core/routines/engine.js +175 -0
- package/dist/core/routines/incidents.d.ts +13 -0
- package/dist/core/routines/incidents.js +35 -0
- package/dist/core/routines/schema.d.ts +418 -0
- package/dist/core/routines/schema.js +113 -0
- package/dist/core/routines/signals-collector.d.ts +35 -0
- package/dist/core/routines/signals-collector.js +114 -0
- package/dist/core/routines/store.d.ts +316 -0
- package/dist/core/routines/store.js +99 -0
- package/dist/core/routines/test-utils.d.ts +2 -0
- package/dist/core/routines/test-utils.js +13 -0
- package/dist/core/secrets-audit.d.ts +21 -0
- package/dist/core/secrets-audit.js +60 -0
- package/dist/core/secrets-metadata.d.ts +39 -0
- package/dist/core/secrets-metadata.js +82 -0
- package/dist/core/secrets-motd.d.ts +20 -0
- package/dist/core/secrets-motd.js +72 -0
- package/dist/core/secrets-ops.d.ts +3 -1
- package/dist/core/secrets-ops.js +78 -13
- package/dist/core/secrets-providers.d.ts +50 -0
- package/dist/core/secrets-providers.js +291 -0
- package/dist/core/secrets-rotation.d.ts +52 -0
- package/dist/core/secrets-rotation.js +165 -0
- package/dist/core/secrets-snapshots.d.ts +26 -0
- package/dist/core/secrets-snapshots.js +95 -0
- package/dist/core/secrets-validate.js +2 -1
- package/dist/core/secrets.d.ts +12 -1
- package/dist/core/secrets.js +35 -24
- package/dist/core/self-update.d.ts +41 -0
- package/dist/core/self-update.js +73 -0
- package/dist/core/systemd.js +29 -12
- package/dist/core/telegram.d.ts +6 -0
- package/dist/core/telegram.js +32 -0
- package/dist/core/validate.d.ts +7 -0
- package/dist/core/validate.js +42 -0
- package/dist/index.js +0 -4
- package/dist/mcp/deps-tools.js +9 -1
- package/dist/mcp/git-tools.js +4 -4
- package/dist/mcp/server.js +193 -8
- package/dist/templates/systemd.js +3 -3
- package/dist/templates/unseal.js +5 -1
- package/dist/tui/components/Confirm.js +3 -4
- package/dist/tui/components/Header.js +37 -8
- package/dist/tui/components/KeyHint.js +14 -5
- package/dist/tui/exec-bridge.js +26 -12
- package/dist/tui/hooks/use-fleet-data.js +5 -2
- package/dist/tui/hooks/use-health.js +5 -2
- package/dist/tui/hooks/use-terminal-size.d.ts +1 -0
- package/dist/tui/hooks/use-terminal-size.js +1 -0
- package/dist/tui/router.js +133 -8
- package/dist/tui/routines/RoutinesApp.d.ts +8 -0
- package/dist/tui/routines/RoutinesApp.js +277 -0
- package/dist/tui/routines/components/AlertsPanel.d.ts +7 -0
- package/dist/tui/routines/components/AlertsPanel.js +22 -0
- package/dist/tui/routines/components/AlertsPanel.test.d.ts +1 -0
- package/dist/tui/routines/components/AlertsPanel.test.js +52 -0
- package/dist/tui/routines/components/CommandPalette.d.ts +12 -0
- package/dist/tui/routines/components/CommandPalette.js +21 -0
- package/dist/tui/routines/components/LiveRunPanel.d.ts +12 -0
- package/dist/tui/routines/components/LiveRunPanel.js +107 -0
- package/dist/tui/routines/components/RoutineForm.d.ts +8 -0
- package/dist/tui/routines/components/RoutineForm.js +254 -0
- package/dist/tui/routines/components/SignalsGrid.d.ts +13 -0
- package/dist/tui/routines/components/SignalsGrid.js +34 -0
- package/dist/tui/routines/components/SignalsGrid.test.d.ts +1 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +43 -0
- package/dist/tui/routines/format.d.ts +7 -0
- package/dist/tui/routines/format.js +51 -0
- package/dist/tui/routines/hooks/use-git-fleet.d.ts +33 -0
- package/dist/tui/routines/hooks/use-git-fleet.js +82 -0
- package/dist/tui/routines/hooks/use-logs-stream.d.ts +13 -0
- package/dist/tui/routines/hooks/use-logs-stream.js +64 -0
- package/dist/tui/routines/hooks/use-ops-fleet.d.ts +20 -0
- package/dist/tui/routines/hooks/use-ops-fleet.js +70 -0
- package/dist/tui/routines/hooks/use-repo-detail.d.ts +31 -0
- package/dist/tui/routines/hooks/use-repo-detail.js +104 -0
- package/dist/tui/routines/hooks/use-security.d.ts +33 -0
- package/dist/tui/routines/hooks/use-security.js +110 -0
- package/dist/tui/routines/hooks/use-signals.d.ts +9 -0
- package/dist/tui/routines/hooks/use-signals.js +60 -0
- package/dist/tui/routines/runtime.d.ts +20 -0
- package/dist/tui/routines/runtime.js +40 -0
- package/dist/tui/routines/tabs/CostTab.d.ts +7 -0
- package/dist/tui/routines/tabs/CostTab.js +24 -0
- package/dist/tui/routines/tabs/DashboardTab.d.ts +15 -0
- package/dist/tui/routines/tabs/DashboardTab.js +10 -0
- package/dist/tui/routines/tabs/GitTab.d.ts +6 -0
- package/dist/tui/routines/tabs/GitTab.js +39 -0
- package/dist/tui/routines/tabs/LogsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/LogsTab.js +58 -0
- package/dist/tui/routines/tabs/OpsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/OpsTab.js +34 -0
- package/dist/tui/routines/tabs/RepoDetailView.d.ts +6 -0
- package/dist/tui/routines/tabs/RepoDetailView.js +12 -0
- package/dist/tui/routines/tabs/RoutinesTab.d.ts +10 -0
- package/dist/tui/routines/tabs/RoutinesTab.js +58 -0
- package/dist/tui/routines/tabs/ScaffoldTab.d.ts +2 -0
- package/dist/tui/routines/tabs/ScaffoldTab.js +127 -0
- package/dist/tui/routines/tabs/SecurityTab.d.ts +6 -0
- package/dist/tui/routines/tabs/SecurityTab.js +31 -0
- package/dist/tui/routines/tabs/SettingsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/SettingsTab.js +61 -0
- package/dist/tui/routines/tabs/TimelineTab.d.ts +7 -0
- package/dist/tui/routines/tabs/TimelineTab.js +26 -0
- package/dist/tui/state.js +16 -1
- package/dist/tui/tests/flicker.test.d.ts +1 -0
- package/dist/tui/tests/flicker.test.js +105 -0
- package/dist/tui/tests/keyboard-integration.test.d.ts +1 -0
- package/dist/tui/tests/keyboard-integration.test.js +120 -0
- package/dist/tui/tests/test-app.d.ts +4 -0
- package/dist/tui/tests/test-app.js +79 -0
- package/dist/tui/types.d.ts +14 -1
- package/dist/tui/views/AppDetail.js +40 -26
- package/dist/tui/views/Dashboard.js +34 -9
- package/dist/tui/views/HealthView.js +42 -12
- package/dist/tui/views/LogsView.js +38 -10
- package/dist/tui/views/MultiLogsView.d.ts +2 -0
- package/dist/tui/views/MultiLogsView.js +165 -0
- package/dist/tui/views/SecretEdit.js +18 -7
- package/dist/tui/views/SecretsView.js +55 -39
- package/dist/ui/prompt.d.ts +52 -0
- package/dist/ui/prompt.js +169 -0
- package/package.json +33 -5
- package/dist/commands/motd.d.ts +0 -1
- package/dist/commands/motd.js +0 -10
- package/dist/templates/motd.d.ts +0 -1
- package/dist/templates/motd.js +0 -7
- package/dist/tui/components/AppList.d.ts +0 -12
- package/dist/tui/components/AppList.js +0 -32
- package/dist/tui/hooks/use-keyboard.d.ts +0 -1
- package/dist/tui/hooks/use-keyboard.js +0 -44
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
const LEVEL_PATTERNS = [
|
|
4
|
+
[/\b(error|err|failed|fatal)\b/i, 'error'],
|
|
5
|
+
[/\b(warn|warning)\b/i, 'warn'],
|
|
6
|
+
[/\b(debug|trace)\b/i, 'debug'],
|
|
7
|
+
];
|
|
8
|
+
function classify(line) {
|
|
9
|
+
for (const [pattern, level] of LEVEL_PATTERNS) {
|
|
10
|
+
if (pattern.test(line))
|
|
11
|
+
return level;
|
|
12
|
+
}
|
|
13
|
+
return 'info';
|
|
14
|
+
}
|
|
15
|
+
export function useLogsStream(opts) {
|
|
16
|
+
const [lines, setLines] = useState([]);
|
|
17
|
+
const [running, setRunning] = useState(false);
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [version, setVersion] = useState(0);
|
|
20
|
+
const bufferSize = opts?.bufferSize ?? 500;
|
|
21
|
+
const lineBufferRef = useRef('');
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!opts) {
|
|
24
|
+
setLines([]);
|
|
25
|
+
setRunning(false);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setLines([]);
|
|
29
|
+
setError(null);
|
|
30
|
+
setRunning(true);
|
|
31
|
+
lineBufferRef.current = '';
|
|
32
|
+
const child = spawn(opts.command, opts.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
33
|
+
const append = (chunk) => {
|
|
34
|
+
lineBufferRef.current += chunk;
|
|
35
|
+
let idx;
|
|
36
|
+
const newLines = [];
|
|
37
|
+
while ((idx = lineBufferRef.current.indexOf('\n')) >= 0) {
|
|
38
|
+
const text = lineBufferRef.current.slice(0, idx);
|
|
39
|
+
lineBufferRef.current = lineBufferRef.current.slice(idx + 1);
|
|
40
|
+
if (!text.trim())
|
|
41
|
+
continue;
|
|
42
|
+
newLines.push({ text, level: classify(text), timestamp: new Date() });
|
|
43
|
+
}
|
|
44
|
+
if (newLines.length > 0) {
|
|
45
|
+
setLines(prev => {
|
|
46
|
+
const combined = [...prev, ...newLines];
|
|
47
|
+
return combined.length > bufferSize ? combined.slice(-bufferSize) : combined;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
child.stdout?.setEncoding('utf-8');
|
|
52
|
+
child.stderr?.setEncoding('utf-8');
|
|
53
|
+
child.stdout?.on('data', append);
|
|
54
|
+
child.stderr?.on('data', append);
|
|
55
|
+
child.on('error', err => { setError(err.message); setRunning(false); });
|
|
56
|
+
child.on('close', () => setRunning(false));
|
|
57
|
+
return () => {
|
|
58
|
+
child.kill('SIGTERM');
|
|
59
|
+
setTimeout(() => { if (!child.killed)
|
|
60
|
+
child.kill('SIGKILL'); }, 2000);
|
|
61
|
+
};
|
|
62
|
+
}, [opts?.command, opts?.args.join(' '), bufferSize, version]);
|
|
63
|
+
return { lines, running, error, restart: () => setVersion(v => v + 1) };
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AppEntry } from '../../../core/registry.js';
|
|
2
|
+
import { type ServiceStatus } from '../../../core/systemd.js';
|
|
3
|
+
export interface OpsRepoState {
|
|
4
|
+
name: string;
|
|
5
|
+
service: ServiceStatus | null;
|
|
6
|
+
runningContainers: number;
|
|
7
|
+
totalContainers: number;
|
|
8
|
+
}
|
|
9
|
+
export interface OpsSnapshot {
|
|
10
|
+
loading: boolean;
|
|
11
|
+
repos: OpsRepoState[];
|
|
12
|
+
nginxSites: number | null;
|
|
13
|
+
nginxOk: boolean | null;
|
|
14
|
+
dockerDatabasesActive: boolean | null;
|
|
15
|
+
diskPercent: number | null;
|
|
16
|
+
refreshedAt: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function useOpsFleet(apps: AppEntry[]): OpsSnapshot & {
|
|
19
|
+
refresh(): void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { execSafe } from '../../../core/exec.js';
|
|
3
|
+
import { getMultipleServiceStatuses } from '../../../core/systemd.js';
|
|
4
|
+
function countContainersByProject(project) {
|
|
5
|
+
const res = execSafe('docker', [
|
|
6
|
+
'ps', '--all',
|
|
7
|
+
'--filter', `label=com.docker.compose.project=${project}`,
|
|
8
|
+
'--format', '{{.State}}',
|
|
9
|
+
], { timeout: 5000 });
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
return { running: 0, total: 0 };
|
|
12
|
+
const states = res.stdout.split('\n').map(s => s.trim()).filter(Boolean);
|
|
13
|
+
return { running: states.filter(s => s === 'running').length, total: states.length };
|
|
14
|
+
}
|
|
15
|
+
function nginxHealth() {
|
|
16
|
+
const test = execSafe('nginx', ['-t'], { timeout: 4000 });
|
|
17
|
+
const list = execSafe('bash', ['-c', "ls /etc/nginx/sites-enabled/ 2>/dev/null | wc -l"], { timeout: 3000 });
|
|
18
|
+
const count = list.ok ? parseInt(list.stdout.trim(), 10) : null;
|
|
19
|
+
return { sites: Number.isFinite(count) ? count : null, ok: test.ok };
|
|
20
|
+
}
|
|
21
|
+
function diskPercent(path) {
|
|
22
|
+
const res = execSafe('bash', ['-c', `df -P ${path} | awk 'NR==2 {gsub("%",""); print $5}'`], { timeout: 3000 });
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
return null;
|
|
25
|
+
const v = parseInt(res.stdout.trim(), 10);
|
|
26
|
+
return Number.isFinite(v) ? v : null;
|
|
27
|
+
}
|
|
28
|
+
export function useOpsFleet(apps) {
|
|
29
|
+
const [state, setState] = useState({
|
|
30
|
+
loading: false,
|
|
31
|
+
repos: [],
|
|
32
|
+
nginxSites: null,
|
|
33
|
+
nginxOk: null,
|
|
34
|
+
dockerDatabasesActive: null,
|
|
35
|
+
diskPercent: null,
|
|
36
|
+
refreshedAt: 0,
|
|
37
|
+
});
|
|
38
|
+
const load = () => {
|
|
39
|
+
setState(s => ({ ...s, loading: true }));
|
|
40
|
+
const serviceNames = apps.map(a => a.serviceName).filter(Boolean);
|
|
41
|
+
const allServiceNames = Array.from(new Set([...serviceNames, 'docker-databases']));
|
|
42
|
+
const serviceMap = getMultipleServiceStatuses(allServiceNames);
|
|
43
|
+
const repos = apps.map(app => {
|
|
44
|
+
const counts = countContainersByProject(app.name);
|
|
45
|
+
return {
|
|
46
|
+
name: app.name,
|
|
47
|
+
service: serviceMap.get(app.serviceName) ?? null,
|
|
48
|
+
runningContainers: counts.running,
|
|
49
|
+
totalContainers: counts.total,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
const nginx = nginxHealth();
|
|
53
|
+
const disk = diskPercent('/home');
|
|
54
|
+
setState({
|
|
55
|
+
loading: false,
|
|
56
|
+
repos,
|
|
57
|
+
nginxSites: nginx.sites,
|
|
58
|
+
nginxOk: nginx.ok,
|
|
59
|
+
dockerDatabasesActive: serviceMap.get('docker-databases')?.active ?? null,
|
|
60
|
+
diskPercent: disk,
|
|
61
|
+
refreshedAt: Date.now(),
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
load();
|
|
66
|
+
const id = setInterval(load, 20_000);
|
|
67
|
+
return () => clearInterval(id);
|
|
68
|
+
}, [apps.map(a => a.name).join('|')]);
|
|
69
|
+
return { ...state, refresh: load };
|
|
70
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AppEntry } from '../../../core/registry.js';
|
|
2
|
+
import { type GitStatus } from '../../../core/git.js';
|
|
3
|
+
import { type ServiceStatus } from '../../../core/systemd.js';
|
|
4
|
+
export interface OpenPr {
|
|
5
|
+
number: number;
|
|
6
|
+
title: string;
|
|
7
|
+
author: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
url: string;
|
|
10
|
+
isDraft: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface LastCommit {
|
|
13
|
+
hash: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
date: string;
|
|
16
|
+
author: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RepoDetailSnapshot {
|
|
19
|
+
loading: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
git: GitStatus | null;
|
|
22
|
+
lastCommit: LastCommit | null;
|
|
23
|
+
openPrs: OpenPr[] | null;
|
|
24
|
+
service: ServiceStatus | null;
|
|
25
|
+
runningContainers: number | null;
|
|
26
|
+
totalContainers: number | null;
|
|
27
|
+
refreshedAt: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function useRepoDetail(app: AppEntry | null): RepoDetailSnapshot & {
|
|
30
|
+
refresh(): void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { execSafe } from '../../../core/exec.js';
|
|
3
|
+
import { getGitStatus } from '../../../core/git.js';
|
|
4
|
+
import { getMultipleServiceStatuses } from '../../../core/systemd.js';
|
|
5
|
+
function fetchLastCommit(cwd) {
|
|
6
|
+
const res = execSafe('git', ['-C', cwd, 'log', '-1', '--format=%H%x09%s%x09%ad%x09%an', '--date=iso-strict'], { timeout: 5000 });
|
|
7
|
+
if (!res.ok || !res.stdout)
|
|
8
|
+
return null;
|
|
9
|
+
const [hash, subject, date, author] = res.stdout.split('\t');
|
|
10
|
+
if (!hash || !subject)
|
|
11
|
+
return null;
|
|
12
|
+
return { hash: hash.slice(0, 8), subject, date, author };
|
|
13
|
+
}
|
|
14
|
+
function fetchOpenPrs(cwd) {
|
|
15
|
+
const res = execSafe('gh', [
|
|
16
|
+
'pr', 'list', '--state', 'open',
|
|
17
|
+
'--json', 'number,title,author,updatedAt,url,isDraft',
|
|
18
|
+
'--limit', '20',
|
|
19
|
+
], { cwd, timeout: 8000 });
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(res.stdout);
|
|
24
|
+
return raw.map(p => ({
|
|
25
|
+
number: p.number,
|
|
26
|
+
title: p.title,
|
|
27
|
+
author: p.author?.login ?? 'unknown',
|
|
28
|
+
updatedAt: p.updatedAt,
|
|
29
|
+
url: p.url,
|
|
30
|
+
isDraft: p.isDraft,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function fetchContainerCounts(project) {
|
|
38
|
+
const res = execSafe('docker', [
|
|
39
|
+
'ps', '--all',
|
|
40
|
+
'--filter', `label=com.docker.compose.project=${project}`,
|
|
41
|
+
'--format', '{{.State}}',
|
|
42
|
+
], { timeout: 5000 });
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
return null;
|
|
45
|
+
const states = res.stdout.split('\n').map(s => s.trim()).filter(Boolean);
|
|
46
|
+
return { running: states.filter(s => s === 'running').length, total: states.length };
|
|
47
|
+
}
|
|
48
|
+
export function useRepoDetail(app) {
|
|
49
|
+
const [snapshot, setSnapshot] = useState({
|
|
50
|
+
loading: false,
|
|
51
|
+
error: null,
|
|
52
|
+
git: null,
|
|
53
|
+
lastCommit: null,
|
|
54
|
+
openPrs: null,
|
|
55
|
+
service: null,
|
|
56
|
+
runningContainers: null,
|
|
57
|
+
totalContainers: null,
|
|
58
|
+
refreshedAt: 0,
|
|
59
|
+
});
|
|
60
|
+
const mounted = useRef(true);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
mounted.current = true;
|
|
63
|
+
return () => { mounted.current = false; };
|
|
64
|
+
}, []);
|
|
65
|
+
const load = () => {
|
|
66
|
+
if (!app)
|
|
67
|
+
return;
|
|
68
|
+
setSnapshot(s => ({ ...s, loading: true, error: null }));
|
|
69
|
+
try {
|
|
70
|
+
const cwd = app.composePath ?? '';
|
|
71
|
+
const git = cwd ? getGitStatus(cwd) : null;
|
|
72
|
+
const lastCommit = cwd ? fetchLastCommit(cwd) : null;
|
|
73
|
+
const openPrs = cwd ? fetchOpenPrs(cwd) : null;
|
|
74
|
+
const service = app.serviceName ? getMultipleServiceStatuses([app.serviceName]).get(app.serviceName) ?? null : null;
|
|
75
|
+
const containers = fetchContainerCounts(app.name);
|
|
76
|
+
if (!mounted.current)
|
|
77
|
+
return;
|
|
78
|
+
setSnapshot({
|
|
79
|
+
loading: false,
|
|
80
|
+
error: null,
|
|
81
|
+
git,
|
|
82
|
+
lastCommit,
|
|
83
|
+
openPrs,
|
|
84
|
+
service,
|
|
85
|
+
runningContainers: containers?.running ?? null,
|
|
86
|
+
totalContainers: containers?.total ?? null,
|
|
87
|
+
refreshedAt: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (!mounted.current)
|
|
92
|
+
return;
|
|
93
|
+
setSnapshot(s => ({ ...s, loading: false, error: err.message }));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!app)
|
|
98
|
+
return;
|
|
99
|
+
load();
|
|
100
|
+
const id = setInterval(load, 30_000);
|
|
101
|
+
return () => clearInterval(id);
|
|
102
|
+
}, [app?.name]);
|
|
103
|
+
return { ...snapshot, refresh: load };
|
|
104
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AppEntry } from '../../../core/registry.js';
|
|
2
|
+
export interface GuardianStatus {
|
|
3
|
+
binaryInstalled: boolean;
|
|
4
|
+
whitelistExists: boolean;
|
|
5
|
+
whitelistLines: number | null;
|
|
6
|
+
runcWhitelisted: boolean | null;
|
|
7
|
+
}
|
|
8
|
+
export interface SshAgentStatus {
|
|
9
|
+
socketExists: boolean;
|
|
10
|
+
keyLoaded: boolean | null;
|
|
11
|
+
keyFingerprint: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface CertExpiry {
|
|
14
|
+
domain: string;
|
|
15
|
+
expiresAt: string | null;
|
|
16
|
+
daysUntil: number | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SecretAge {
|
|
19
|
+
app: string;
|
|
20
|
+
ageDays: number | null;
|
|
21
|
+
error: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface SecuritySnapshot {
|
|
24
|
+
loading: boolean;
|
|
25
|
+
guardian: GuardianStatus | null;
|
|
26
|
+
ssh: SshAgentStatus | null;
|
|
27
|
+
certs: CertExpiry[];
|
|
28
|
+
secretAges: SecretAge[];
|
|
29
|
+
refreshedAt: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function useSecurity(apps: AppEntry[]): SecuritySnapshot & {
|
|
32
|
+
refresh(): void;
|
|
33
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { execSafe } from '../../../core/exec.js';
|
|
4
|
+
const SSH_AUTH_SOCK_PATH = '/tmp/fleet-ssh-agent.sock';
|
|
5
|
+
const GUARDIAN_WHITELIST = '/etc/guardian/whitelist';
|
|
6
|
+
function checkGuardian() {
|
|
7
|
+
const binary = existsSync('/usr/local/bin/guardiand');
|
|
8
|
+
const whitelistExists = existsSync(GUARDIAN_WHITELIST);
|
|
9
|
+
let whitelistLines = null;
|
|
10
|
+
let runcWhitelisted = null;
|
|
11
|
+
if (whitelistExists) {
|
|
12
|
+
const res = execSafe('bash', ['-c', `wc -l < ${GUARDIAN_WHITELIST} && grep -c '^/runc$' ${GUARDIAN_WHITELIST}`], { timeout: 3000 });
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
const lines = res.stdout.split('\n').map(s => parseInt(s.trim(), 10));
|
|
15
|
+
whitelistLines = Number.isFinite(lines[0]) ? lines[0] : null;
|
|
16
|
+
runcWhitelisted = lines[1] ? lines[1] > 0 : false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { binaryInstalled: binary, whitelistExists, whitelistLines, runcWhitelisted };
|
|
20
|
+
}
|
|
21
|
+
function checkSshAgent() {
|
|
22
|
+
const socketExists = existsSync(SSH_AUTH_SOCK_PATH);
|
|
23
|
+
if (!socketExists)
|
|
24
|
+
return { socketExists: false, keyLoaded: null, keyFingerprint: null };
|
|
25
|
+
const res = execSafe('ssh-add', ['-l'], {
|
|
26
|
+
env: { SSH_AUTH_SOCK: SSH_AUTH_SOCK_PATH },
|
|
27
|
+
timeout: 3000,
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok || !res.stdout)
|
|
30
|
+
return { socketExists: true, keyLoaded: false, keyFingerprint: null };
|
|
31
|
+
const line = res.stdout.split('\n')[0] ?? '';
|
|
32
|
+
const fp = line.split(/\s+/)[1] ?? null;
|
|
33
|
+
return { socketExists: true, keyLoaded: true, keyFingerprint: fp };
|
|
34
|
+
}
|
|
35
|
+
function certExpiryFor(domain) {
|
|
36
|
+
const paths = [
|
|
37
|
+
`/etc/letsencrypt/live/${domain}/fullchain.pem`,
|
|
38
|
+
`/etc/nginx/ssl/${domain}/fullchain.pem`,
|
|
39
|
+
];
|
|
40
|
+
for (const path of paths) {
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
continue;
|
|
43
|
+
const res = execSafe('openssl', ['x509', '-enddate', '-noout', '-in', path], { timeout: 3000 });
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
continue;
|
|
46
|
+
const m = res.stdout.match(/notAfter=(.+)/);
|
|
47
|
+
if (!m)
|
|
48
|
+
continue;
|
|
49
|
+
const expiresAt = new Date(m[1]);
|
|
50
|
+
if (isNaN(expiresAt.getTime()))
|
|
51
|
+
continue;
|
|
52
|
+
const daysUntil = Math.floor((expiresAt.getTime() - Date.now()) / 86_400_000);
|
|
53
|
+
return { domain, expiresAt: expiresAt.toISOString(), daysUntil };
|
|
54
|
+
}
|
|
55
|
+
return { domain, expiresAt: null, daysUntil: null };
|
|
56
|
+
}
|
|
57
|
+
function secretAgeFor(app) {
|
|
58
|
+
const secretsPath = `${app.composePath}/secrets/vault.age`;
|
|
59
|
+
if (!existsSync(secretsPath))
|
|
60
|
+
return { app: app.name, ageDays: null, error: 'no vault.age' };
|
|
61
|
+
try {
|
|
62
|
+
const stat = statSync(secretsPath);
|
|
63
|
+
const ageDays = Math.floor((Date.now() - stat.mtimeMs) / 86_400_000);
|
|
64
|
+
return { app: app.name, ageDays, error: null };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { app: app.name, ageDays: null, error: err.message };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function useSecurity(apps) {
|
|
71
|
+
const [state, setState] = useState({
|
|
72
|
+
loading: false,
|
|
73
|
+
guardian: null,
|
|
74
|
+
ssh: null,
|
|
75
|
+
certs: [],
|
|
76
|
+
secretAges: [],
|
|
77
|
+
refreshedAt: 0,
|
|
78
|
+
});
|
|
79
|
+
const load = () => {
|
|
80
|
+
setState(s => ({ ...s, loading: true }));
|
|
81
|
+
const guardian = checkGuardian();
|
|
82
|
+
const ssh = checkSshAgent();
|
|
83
|
+
const domains = new Set();
|
|
84
|
+
for (const app of apps)
|
|
85
|
+
for (const d of app.domains)
|
|
86
|
+
domains.add(d);
|
|
87
|
+
const certs = [];
|
|
88
|
+
for (const d of domains)
|
|
89
|
+
certs.push(certExpiryFor(d));
|
|
90
|
+
certs.sort((a, b) => (a.daysUntil ?? Infinity) - (b.daysUntil ?? Infinity));
|
|
91
|
+
const secretAges = apps
|
|
92
|
+
.filter(a => a.secretsManaged)
|
|
93
|
+
.map(secretAgeFor)
|
|
94
|
+
.sort((a, b) => (b.ageDays ?? 0) - (a.ageDays ?? 0));
|
|
95
|
+
setState({
|
|
96
|
+
loading: false,
|
|
97
|
+
guardian,
|
|
98
|
+
ssh,
|
|
99
|
+
certs,
|
|
100
|
+
secretAges,
|
|
101
|
+
refreshedAt: Date.now(),
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
load();
|
|
106
|
+
const id = setInterval(load, 60_000);
|
|
107
|
+
return () => clearInterval(id);
|
|
108
|
+
}, [apps.map(a => a.name).join('|')]);
|
|
109
|
+
return { ...state, refresh: load };
|
|
110
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Signal } from '../../../core/routines/schema.js';
|
|
2
|
+
import type { SignalCollector, SignalTarget } from '../../../core/routines/signals-collector.js';
|
|
3
|
+
export interface UseSignalsResult {
|
|
4
|
+
snapshot: Map<string, Signal[]>;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
lastRefreshed: number;
|
|
7
|
+
refresh(force?: boolean): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function useSignals(collector: SignalCollector, targets: SignalTarget[], intervalMs?: number): UseSignalsResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
export function useSignals(collector, targets, intervalMs = 30_000) {
|
|
3
|
+
const [snapshot, setSnapshot] = useState(new Map());
|
|
4
|
+
const [loading, setLoading] = useState(false);
|
|
5
|
+
const [lastRefreshed, setLastRefreshed] = useState(0);
|
|
6
|
+
const mounted = useRef(true);
|
|
7
|
+
const targetsKey = targets.map(t => t.repoName).join('|');
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
mounted.current = true;
|
|
10
|
+
return () => { mounted.current = false; };
|
|
11
|
+
}, []);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
const run = async (force = false) => {
|
|
15
|
+
if (cancelled)
|
|
16
|
+
return;
|
|
17
|
+
setLoading(true);
|
|
18
|
+
try {
|
|
19
|
+
if (force) {
|
|
20
|
+
await collector.collect(targets.map(target => ({ target, force: true })));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
await collector.collect(targets.map(target => ({ target })));
|
|
24
|
+
}
|
|
25
|
+
if (cancelled || !mounted.current)
|
|
26
|
+
return;
|
|
27
|
+
const next = new Map();
|
|
28
|
+
for (const t of targets)
|
|
29
|
+
next.set(t.repoName, collector.readCached(t.repoName));
|
|
30
|
+
setSnapshot(next);
|
|
31
|
+
setLastRefreshed(Date.now());
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
if (mounted.current && !cancelled)
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
void run();
|
|
39
|
+
const id = setInterval(() => void run(), intervalMs);
|
|
40
|
+
return () => {
|
|
41
|
+
cancelled = true;
|
|
42
|
+
clearInterval(id);
|
|
43
|
+
};
|
|
44
|
+
}, [collector, targetsKey, intervalMs]);
|
|
45
|
+
const refresh = async (force = false) => {
|
|
46
|
+
setLoading(true);
|
|
47
|
+
try {
|
|
48
|
+
await collector.collect(targets.map(target => ({ target, force })));
|
|
49
|
+
const next = new Map();
|
|
50
|
+
for (const t of targets)
|
|
51
|
+
next.set(t.repoName, collector.readCached(t.repoName));
|
|
52
|
+
setSnapshot(next);
|
|
53
|
+
setLastRefreshed(Date.now());
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return { snapshot, loading, lastRefreshed, refresh };
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { RoutineEngine } from '../../core/routines/engine.js';
|
|
3
|
+
import { RoutineStore } from '../../core/routines/store.js';
|
|
4
|
+
import { SignalCollector } from '../../core/routines/signals-collector.js';
|
|
5
|
+
export interface RoutinesRuntime {
|
|
6
|
+
engine: RoutineEngine;
|
|
7
|
+
store: RoutineStore;
|
|
8
|
+
collector: SignalCollector;
|
|
9
|
+
db: Database.Database;
|
|
10
|
+
seeded: {
|
|
11
|
+
seeded: number;
|
|
12
|
+
skipped: number;
|
|
13
|
+
};
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
export interface RuntimeOptions {
|
|
17
|
+
dataDir?: string;
|
|
18
|
+
seedDefaults?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function createRuntime(opts?: RuntimeOptions): RoutinesRuntime;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { createStdoutNotifier } from '../../adapters/notifier/index.js';
|
|
4
|
+
import { builtInSignalProviders } from '../../adapters/signals/index.js';
|
|
5
|
+
import { closeDb, openDb } from '../../core/routines/db.js';
|
|
6
|
+
import { builtInDefaultRoutines } from '../../core/routines/defaults.js';
|
|
7
|
+
import { RoutineEngine as Engine } from '../../core/routines/engine.js';
|
|
8
|
+
import { createClaudeCliRunner } from '../../adapters/runner/claude-cli.js';
|
|
9
|
+
import { createMcpCallRunner } from '../../adapters/runner/mcp-call.js';
|
|
10
|
+
import { createShellRunner } from '../../adapters/runner/shell.js';
|
|
11
|
+
import { createSystemdTimerAdapter } from '../../adapters/scheduler/systemd-timer.js';
|
|
12
|
+
import { RoutineStore } from '../../core/routines/store.js';
|
|
13
|
+
import { SignalCollector } from '../../core/routines/signals-collector.js';
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const DEFAULT_DATA_DIR = join(__dirname, '..', '..', '..', 'data');
|
|
16
|
+
export function createRuntime(opts = {}) {
|
|
17
|
+
const dataDir = opts.dataDir ?? DEFAULT_DATA_DIR;
|
|
18
|
+
const db = openDb({ path: join(dataDir, 'fleet.db') });
|
|
19
|
+
const store = new RoutineStore(join(dataDir, 'routines.json'));
|
|
20
|
+
const seeded = opts.seedDefaults === false
|
|
21
|
+
? { seeded: 0, skipped: 0 }
|
|
22
|
+
: store.seedDefaults(builtInDefaultRoutines());
|
|
23
|
+
const scheduler = createSystemdTimerAdapter();
|
|
24
|
+
const engine = new Engine({
|
|
25
|
+
store,
|
|
26
|
+
db,
|
|
27
|
+
runners: [createShellRunner(), createClaudeCliRunner(), createMcpCallRunner()],
|
|
28
|
+
scheduler: scheduler.available() ? scheduler : null,
|
|
29
|
+
notifiers: [createStdoutNotifier()],
|
|
30
|
+
});
|
|
31
|
+
const collector = new SignalCollector({ providers: builtInSignalProviders(), db, concurrency: 4 });
|
|
32
|
+
return {
|
|
33
|
+
engine,
|
|
34
|
+
store,
|
|
35
|
+
collector,
|
|
36
|
+
db,
|
|
37
|
+
seeded,
|
|
38
|
+
close: () => closeDb(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { RoutineEngine } from '../../../core/routines/engine.js';
|
|
3
|
+
export interface CostTabProps {
|
|
4
|
+
engine: RoutineEngine;
|
|
5
|
+
dailyBudgetUsd?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function CostTab({ engine, dailyBudgetUsd }: CostTabProps): React.JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { LineChart } from '@matthesketh/ink-chart';
|
|
5
|
+
import { costByRoutine, costRollup, dailyCostSeries } from '../../../core/routines/cost-queries.js';
|
|
6
|
+
import { formatUsd, truncate } from '../../../tui/routines/format.js';
|
|
7
|
+
function usdColor(usd, soft = 1, hard = 5) {
|
|
8
|
+
if (usd >= hard)
|
|
9
|
+
return 'red';
|
|
10
|
+
if (usd >= soft)
|
|
11
|
+
return 'yellow';
|
|
12
|
+
return 'green';
|
|
13
|
+
}
|
|
14
|
+
export function CostTab({ engine, dailyBudgetUsd = 10 }) {
|
|
15
|
+
const { rollup, byRoutine, daily } = useMemo(() => ({
|
|
16
|
+
rollup: costRollup(engine.db),
|
|
17
|
+
byRoutine: costByRoutine(engine.db, 30, 10),
|
|
18
|
+
daily: dailyCostSeries(engine.db, 14),
|
|
19
|
+
}), [engine.db]);
|
|
20
|
+
const projectedDaily = rollup.usdToday;
|
|
21
|
+
const dailyBudgetExceeded = projectedDaily > dailyBudgetUsd;
|
|
22
|
+
const series = daily.map(d => d.usd);
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Cost / usage" }), _jsxs(Text, { color: usdColor(rollup.usdToday, dailyBudgetUsd / 2, dailyBudgetUsd), children: ["today ", formatUsd(rollup.usdToday)] }), _jsxs(Text, { color: "gray", children: ["week ", formatUsd(rollup.usdWeek)] }), _jsxs(Text, { color: "gray", children: ["month ", formatUsd(rollup.usdMonth)] }), dailyBudgetExceeded && (_jsxs(Text, { color: "red", bold: true, children: ["over daily budget (", formatUsd(dailyBudgetUsd), ")"] }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Daily spend (last 14 days)" }), series.every(v => v === 0) ? (_jsx(Text, { color: "gray", children: " no claude-cli runs with cost yet" })) : (_jsx(LineChart, { data: series, width: 60, height: 8, color: "cyan", showAxis: true, label: "USD / day" }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Top routines by spend (30d)" }), byRoutine.length === 0 && _jsx(Text, { color: "gray", children: " no runs yet" }), _jsxs(Box, { children: [_jsx(Box, { width: 24, children: _jsx(Text, { bold: true, children: "ROUTINE" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "RUNS" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "USD" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { bold: true, children: "IN TOKENS" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { bold: true, children: "OUT TOKENS" }) }), _jsx(Text, { bold: true, children: "AVG / RUN" })] }), byRoutine.map(row => (_jsxs(Box, { children: [_jsx(Box, { width: 24, children: _jsx(Text, { children: truncate(row.routineId, 22) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { children: row.runs }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: usdColor(row.usd), children: formatUsd(row.usd) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: "gray", children: row.inputTokens.toLocaleString() }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: "gray", children: row.outputTokens.toLocaleString() }) }), _jsx(Text, { color: "gray", children: row.runs > 0 ? formatUsd(row.usd / row.runs) : '—' })] }, row.routineId)))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Last 14 days (buckets)" }), daily.map(bucket => (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: "gray", children: bucket.date }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: usdColor(bucket.usd), children: formatUsd(bucket.usd) }) }), _jsxs(Text, { color: "gray", children: [bucket.runs, " runs"] })] }, bucket.date)))] })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Signal } from '../../../core/routines/schema.js';
|
|
3
|
+
import { type SignalsGridRow } from '../components/SignalsGrid.js';
|
|
4
|
+
export interface DashboardTabProps {
|
|
5
|
+
rows: SignalsGridRow[];
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
lastRefreshed: number;
|
|
9
|
+
signalsByRepo: Map<string, Signal[]>;
|
|
10
|
+
seededNotice: {
|
|
11
|
+
seeded: number;
|
|
12
|
+
skipped: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function DashboardTab({ rows, selectedIndex, loading, lastRefreshed, signalsByRepo, seededNotice, }: DashboardTabProps): React.JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { AlertsPanel } from '../components/AlertsPanel.js';
|
|
5
|
+
import { SignalsGrid } from '../components/SignalsGrid.js';
|
|
6
|
+
import { formatRelative } from '../format.js';
|
|
7
|
+
const SLICE_ONE_KINDS = ['git-clean', 'container-up', 'ci-status'];
|
|
8
|
+
export function DashboardTab({ rows, selectedIndex, loading, lastRefreshed, signalsByRepo, seededNotice, }) {
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Fleet dashboard" }), _jsxs(Text, { color: "gray", children: [rows.length, " repos"] }), loading ? (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " refreshing"] })) : (_jsxs(Text, { color: "gray", children: ["updated ", formatRelative(new Date(lastRefreshed).toISOString())] })), seededNotice.seeded > 0 && (_jsxs(Text, { color: "magenta", children: ["seeded ", seededNotice.seeded, " default routine", seededNotice.seeded === 1 ? '' : 's'] }))] }), _jsx(SignalsGrid, { rows: rows, selectedIndex: selectedIndex, kinds: SLICE_ONE_KINDS }), _jsx(AlertsPanel, { signals: signalsByRepo })] }));
|
|
10
|
+
}
|