@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,39 @@
|
|
|
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 { formatRelative, truncate } from '../../../tui/routines/format.js';
|
|
5
|
+
import { useGitFleet } from '../../../tui/routines/hooks/use-git-fleet.js';
|
|
6
|
+
function prAgeColor(updatedAt) {
|
|
7
|
+
const ageDays = (Date.now() - new Date(updatedAt).getTime()) / 86_400_000;
|
|
8
|
+
if (ageDays >= 14)
|
|
9
|
+
return 'red';
|
|
10
|
+
if (ageDays >= 7)
|
|
11
|
+
return 'yellow';
|
|
12
|
+
return 'gray';
|
|
13
|
+
}
|
|
14
|
+
function reviewBadge(decision) {
|
|
15
|
+
if (decision === 'APPROVED')
|
|
16
|
+
return _jsx(Text, { color: "green", children: "approved" });
|
|
17
|
+
if (decision === 'CHANGES_REQUESTED')
|
|
18
|
+
return _jsx(Text, { color: "yellow", children: "changes" });
|
|
19
|
+
if (decision === 'REVIEW_REQUIRED')
|
|
20
|
+
return _jsx(Text, { color: "cyan", children: "review" });
|
|
21
|
+
return _jsx(Text, { color: "gray", children: "\u2014" });
|
|
22
|
+
}
|
|
23
|
+
function PrRow({ pr }) {
|
|
24
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 20, children: _jsx(Text, { children: truncate(pr.repo, 18) }) }), _jsx(Box, { width: 8, children: _jsxs(Text, { color: pr.isDraft ? 'gray' : 'cyan', children: ["#", pr.number, pr.isDraft ? ' d' : ''] }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: truncate(pr.title, 55) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: "gray", children: pr.author }) }), _jsx(Box, { width: 12, children: reviewBadge(pr.reviewDecision) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: prAgeColor(pr.updatedAt), children: formatRelative(pr.updatedAt) }) })] }));
|
|
25
|
+
}
|
|
26
|
+
function BranchRow({ bs }) {
|
|
27
|
+
const diverged = bs.ahead > 0 || bs.behind > 0;
|
|
28
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 20, children: _jsx(Text, { children: truncate(bs.repo, 18) }) }), _jsx(Box, { width: 18, children: _jsx(Text, { children: bs.branch }) }), _jsx(Box, { width: 10, children: _jsxs(Text, { color: diverged ? 'yellow' : 'gray', children: ["\u2191", bs.ahead, " \u2193", bs.behind] }) }), _jsx(Box, { width: 16, children: _jsx(Text, { color: bs.clean ? 'green' : 'yellow', children: bs.clean ? 'clean' : `${bs.dirtyCount} dirty` }) }), _jsx(Box, { width: 14, children: _jsxs(Text, { color: bs.releasePending > 0 ? 'magenta' : 'gray', children: [bs.releasePending, " to release"] }) })] }));
|
|
29
|
+
}
|
|
30
|
+
export function GitTab({ apps }) {
|
|
31
|
+
const snap = useGitFleet(apps);
|
|
32
|
+
const prsToReview = snap.prs.filter(p => !p.isDraft && p.reviewDecision === 'REVIEW_REQUIRED');
|
|
33
|
+
const prsApproved = snap.prs.filter(p => p.reviewDecision === 'APPROVED');
|
|
34
|
+
const prsChanges = snap.prs.filter(p => p.reviewDecision === 'CHANGES_REQUESTED');
|
|
35
|
+
const prsDraft = snap.prs.filter(p => p.isDraft);
|
|
36
|
+
const stalePrs = snap.prs.filter(p => (Date.now() - new Date(p.updatedAt).getTime()) / 86_400_000 >= 7);
|
|
37
|
+
const releaseReady = snap.branchStates.filter(b => b.releasePending > 0);
|
|
38
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Git across the fleet" }), _jsxs(Text, { color: "gray", children: [apps.length, " repos"] }), _jsxs(Text, { color: "cyan", children: [snap.prs.length, " open PRs"] }), _jsxs(Text, { color: "yellow", children: [stalePrs.length, " stale"] }), _jsxs(Text, { color: "magenta", children: [releaseReady.length, " ready to release"] }), snap.loading && _jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " refreshing"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Review queue (", prsToReview.length, ")"] }), prsToReview.length === 0 && _jsx(Text, { color: "gray", children: " nothing awaiting review" }), prsToReview.slice(0, 6).map(pr => _jsx(PrRow, { pr: pr }, `${pr.repo}-${pr.number}`)), prsApproved.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, color: "green", children: ["Approved (", prsApproved.length, ")"] }), prsApproved.slice(0, 4).map(pr => _jsx(PrRow, { pr: pr }, `${pr.repo}-${pr.number}`))] })), prsChanges.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Changes requested (", prsChanges.length, ")"] }), prsChanges.slice(0, 4).map(pr => _jsx(PrRow, { pr: pr }, `${pr.repo}-${pr.number}`))] })), prsDraft.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, color: "gray", children: ["Drafts (", prsDraft.length, ")"] }), prsDraft.slice(0, 4).map(pr => _jsx(PrRow, { pr: pr }, `${pr.repo}-${pr.number}`))] }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Branch state" }), snap.branchStates.length === 0 && _jsx(Text, { color: "gray", children: " no git repos detected" }), snap.branchStates.map(bs => _jsx(BranchRow, { bs: bs }, bs.repo))] }), releaseReady.length > 0 && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: "Release planner" }), releaseReady.map(bs => (_jsxs(Box, { children: [_jsx(Box, { width: 20, children: _jsx(Text, { children: truncate(bs.repo, 18) }) }), _jsxs(Text, { color: "magenta", children: [bs.releasePending, " commits on develop unshipped to main"] })] }, bs.repo)))] })), snap.errors.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "errors" }), snap.errors.map(e => _jsxs(Text, { color: "red", children: [" \u00B7 ", e.repo, ": ", e.message] }, e.repo))] }))] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { LogViewer } from '@matthesketh/ink-log-viewer';
|
|
6
|
+
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
7
|
+
import { truncate } from '../../../tui/routines/format.js';
|
|
8
|
+
import { useLogsStream } from '../../../tui/routines/hooks/use-logs-stream.js';
|
|
9
|
+
export function LogsTab({ apps }) {
|
|
10
|
+
const services = useMemo(() => {
|
|
11
|
+
const list = apps
|
|
12
|
+
.filter(a => a.serviceName)
|
|
13
|
+
.map(a => ({ name: a.serviceName, displayName: a.displayName || a.name }));
|
|
14
|
+
return [{ name: 'docker-databases', displayName: 'docker-databases (shared)' }, ...list];
|
|
15
|
+
}, [apps]);
|
|
16
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
17
|
+
const [source, setSource] = useState({ kind: 'none' });
|
|
18
|
+
const [filter, setFilter] = useState('');
|
|
19
|
+
const opts = source.kind === 'service'
|
|
20
|
+
? { command: 'journalctl', args: ['-u', source.name, '-f', '-n', '200', '--no-pager'] }
|
|
21
|
+
: source.kind === 'container'
|
|
22
|
+
? { command: 'docker', args: ['logs', '-f', '--tail', '200', source.containerId] }
|
|
23
|
+
: null;
|
|
24
|
+
const stream = useLogsStream(opts);
|
|
25
|
+
useRegisterHandler((input, key) => {
|
|
26
|
+
if (input === 'j' || key.downArrow) {
|
|
27
|
+
setSelectedIdx(i => Math.min(i + 1, services.length - 1));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (input === 'k' || key.upArrow) {
|
|
31
|
+
setSelectedIdx(i => Math.max(i - 1, 0));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (key.return && services[selectedIdx]) {
|
|
35
|
+
setSource({ kind: 'service', name: services[selectedIdx].name });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (input === 'w') {
|
|
39
|
+
setFilter(f => f === 'warn' ? '' : 'warn');
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (input === 'x') {
|
|
43
|
+
setFilter(f => f === 'error' ? '' : 'error');
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (input === 'c') {
|
|
47
|
+
setFilter('');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (key.escape && source.kind !== 'none') {
|
|
51
|
+
setSource({ kind: 'none' });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
const filterPattern = filter ? filter : undefined;
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 34, children: [_jsx(Text, { bold: true, children: "Services" }), services.length === 0 && _jsx(Text, { color: "gray", children: " no services" }), services.map((s, i) => (_jsxs(Box, { children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: i === selectedIdx ? 'cyan' : undefined, children: i === selectedIdx ? '▶' : ' ' }) }), _jsx(Text, { color: source.kind === 'service' && source.name === s.name ? 'green' : undefined, bold: i === selectedIdx, children: truncate(s.displayName, 28) })] }, s.name))), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: "gray", children: "j/k pick \u00B7 Enter tail \u00B7 w warn \u00B7 x error \u00B7 c clear \u00B7 Esc stop" }) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: stream.running ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: source.kind === 'none' ? 'pick a service' : `tail · ${source.kind === 'service' ? source.name : source.containerId}` }), stream.running && _jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " live"] }), filter && _jsxs(Text, { color: "yellow", children: ["filter: ", filter] }), _jsxs(Text, { color: "gray", children: [stream.lines.length, " lines"] })] }), stream.error && _jsxs(Text, { color: "red", children: ["\u2716 ", stream.error] }), stream.lines.length === 0 && !stream.error && source.kind !== 'none' && (_jsx(Text, { color: "gray", children: " (waiting for output\u2026)" })), stream.lines.length > 0 && (_jsx(LogViewer, { lines: stream.lines, height: 20, autoScroll: true, filter: filterPattern, showLevel: true }))] })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { formatRelative, truncate } from '../../../tui/routines/format.js';
|
|
5
|
+
import { useOpsFleet } from '../../../tui/routines/hooks/use-ops-fleet.js';
|
|
6
|
+
function serviceColor(repo) {
|
|
7
|
+
if (!repo.service)
|
|
8
|
+
return 'gray';
|
|
9
|
+
if (!repo.service.active)
|
|
10
|
+
return 'red';
|
|
11
|
+
if (repo.totalContainers === 0)
|
|
12
|
+
return 'yellow';
|
|
13
|
+
return repo.runningContainers === repo.totalContainers ? 'green' : 'yellow';
|
|
14
|
+
}
|
|
15
|
+
function RepoOpsRow({ repo }) {
|
|
16
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 22, children: _jsx(Text, { children: truncate(repo.name, 20) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: serviceColor(repo), children: repo.service ? (repo.service.active ? 'active' : repo.service.state) : '—' }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: repo.service?.enabled ? 'green' : 'gray', children: repo.service?.enabled ? 'enabled' : '—' }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: repo.totalContainers > 0
|
|
17
|
+
? `${repo.runningContainers}/${repo.totalContainers}`
|
|
18
|
+
: _jsx(Text, { color: "gray", children: "\u2014" }) }) })] }));
|
|
19
|
+
}
|
|
20
|
+
function diskColor(pct) {
|
|
21
|
+
if (pct == null)
|
|
22
|
+
return 'gray';
|
|
23
|
+
if (pct >= 90)
|
|
24
|
+
return 'red';
|
|
25
|
+
if (pct >= 75)
|
|
26
|
+
return 'yellow';
|
|
27
|
+
return 'green';
|
|
28
|
+
}
|
|
29
|
+
export function OpsTab({ apps }) {
|
|
30
|
+
const snap = useOpsFleet(apps);
|
|
31
|
+
const downServices = snap.repos.filter(r => r.service && !r.service.active).length;
|
|
32
|
+
const stoppedContainers = snap.repos.filter(r => r.totalContainers > 0 && r.runningContainers < r.totalContainers).length;
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Ops overview" }), _jsxs(Text, { color: downServices > 0 ? 'red' : 'green', children: [downServices, " services down"] }), _jsxs(Text, { color: stoppedContainers > 0 ? 'yellow' : 'green', children: [stoppedContainers, " repos with stopped containers"] }), snap.loading ? (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " refreshing"] })) : (_jsxs(Text, { color: "gray", children: ["updated ", formatRelative(new Date(snap.refreshedAt).toISOString())] }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Infrastructure" }), _jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " docker-databases" }) }), _jsx(Text, { color: snap.dockerDatabasesActive ? 'green' : 'red', children: snap.dockerDatabasesActive ? 'active' : snap.dockerDatabasesActive === false ? 'down' : 'unknown' })] }), _jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " nginx" }) }), _jsx(Text, { color: snap.nginxOk ? 'green' : snap.nginxOk === false ? 'red' : 'gray', children: snap.nginxOk ? 'config valid' : snap.nginxOk === false ? 'config BROKEN' : '—' }), snap.nginxSites != null && _jsxs(Text, { color: "gray", children: [" \u00B7 ", snap.nginxSites, " sites"] })] }), _jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " /home disk" }) }), _jsx(Text, { color: diskColor(snap.diskPercent), children: snap.diskPercent != null ? `${snap.diskPercent}% used` : '—' })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Box, { width: 22, children: _jsx(Text, { bold: true, children: "APP" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { bold: true, children: "SERVICE" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "AUTOSTART" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { bold: true, children: "CONTAINERS" }) })] }), snap.repos.length === 0 && _jsx(Text, { color: "gray", children: " no apps" }), snap.repos.map(r => _jsx(RepoOpsRow, { repo: r }, r.name))] }), _jsx(Text, { color: "gray", children: "fleet-native actions live outside this tab: `fleet restart` / `fleet deploy` / `fleet nginx`" })] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { formatRelative, truncate } from '../../../tui/routines/format.js';
|
|
5
|
+
import { useRepoDetail } from '../../../tui/routines/hooks/use-repo-detail.js';
|
|
6
|
+
function StatValue({ label, value, color }) {
|
|
7
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 14, children: _jsx(Text, { color: "gray", children: label }) }), _jsx(Text, { color: color, children: value })] }));
|
|
8
|
+
}
|
|
9
|
+
export function RepoDetailView({ app }) {
|
|
10
|
+
const snap = useRepoDetail(app);
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: app.name }), _jsx(Text, { color: "gray", children: app.type }), app.domains.length > 0 && _jsx(Text, { color: "yellow", children: app.domains.join(', ') }), snap.loading ? (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " refreshing"] })) : (_jsxs(Text, { color: "gray", children: ["updated ", formatRelative(new Date(snap.refreshedAt).toISOString())] }))] }), snap.error && _jsxs(Text, { color: "red", children: ["\u2716 ", snap.error] }), _jsxs(Box, { flexDirection: "row", gap: 4, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 48, children: [_jsx(Text, { bold: true, children: "Git" }), snap.git === null || !snap.git.initialised ? (_jsx(Text, { color: "gray", children: " not a git repo" })) : (_jsxs(_Fragment, { children: [_jsx(StatValue, { label: "branch", value: snap.git.branch || '—' }), _jsx(StatValue, { label: "ahead/behind", value: `${snap.git.ahead} / ${snap.git.behind}`, color: snap.git.ahead > 0 || snap.git.behind > 0 ? 'yellow' : 'green' }), _jsx(StatValue, { label: "working tree", value: snap.git.clean ? 'clean' : `${snap.git.modified + snap.git.staged + snap.git.untracked} dirty`, color: snap.git.clean ? 'green' : 'yellow' }), _jsx(StatValue, { label: "remote", value: truncate(snap.git.remoteUrl || '—', 32) }), snap.lastCommit && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Last commit" }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [" ", snap.lastCommit.hash, " "] }), _jsx(Text, { children: truncate(snap.lastCommit.subject, 28) })] }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ", snap.lastCommit.author, " \u00B7 ", formatRelative(snap.lastCommit.date)] }) })] }))] }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 48, children: [_jsx(Text, { bold: true, children: "Service" }), snap.service === null ? (_jsx(Text, { color: "gray", children: " no systemd unit" })) : (_jsxs(_Fragment, { children: [_jsx(StatValue, { label: "unit", value: snap.service.name }), _jsx(StatValue, { label: "active", value: snap.service.active ? 'active' : snap.service.state, color: snap.service.active ? 'green' : 'red' }), _jsx(StatValue, { label: "enabled", value: snap.service.enabled ? 'yes' : 'no', color: snap.service.enabled ? 'green' : 'yellow' })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "Containers" }) }), snap.runningContainers === null ? (_jsx(Text, { color: "gray", children: " docker unavailable" })) : snap.totalContainers === 0 ? (_jsx(Text, { color: "yellow", children: " no containers for project" })) : (_jsx(StatValue, { label: "state", value: `${snap.runningContainers}/${snap.totalContainers} running`, color: snap.runningContainers === snap.totalContainers ? 'green' : snap.runningContainers > 0 ? 'yellow' : 'red' }))] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Open PRs ", snap.openPrs ? `(${snap.openPrs.length})` : ''] }), snap.openPrs === null ? (_jsx(Text, { color: "gray", children: " gh unavailable or not a repo" })) : snap.openPrs.length === 0 ? (_jsx(Text, { color: "green", children: " no open PRs" })) : (snap.openPrs.slice(0, 8).map(pr => (_jsxs(Box, { children: [_jsx(Box, { width: 6, children: _jsxs(Text, { color: "cyan", children: ["#", pr.number] }) }), pr.isDraft && _jsx(Box, { width: 8, children: _jsx(Text, { color: "gray", children: "draft" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: truncate(pr.title, 60) }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: "gray", children: pr.author }) }), _jsx(Text, { color: "gray", children: formatRelative(pr.updatedAt) })] }, pr.number))))] }), _jsx(Text, { color: "gray", children: "actions: r restart \u00B7 s shell \u00B7 l logs \u00B7 a run nightly-audit \u00B7 Esc back" })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { RoutineEngine } from '../../../core/routines/engine.js';
|
|
3
|
+
import type { Routine } from '../../../core/routines/schema.js';
|
|
4
|
+
export interface RoutinesTabProps {
|
|
5
|
+
engine: RoutineEngine;
|
|
6
|
+
routines: Routine[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
detailOpen: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function RoutinesTab({ engine, routines, selectedIndex, detailOpen, }: RoutinesTabProps): React.JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { formatDuration, formatRelative, formatUsd, truncate } from '../format.js';
|
|
5
|
+
function summarise(engine, routine) {
|
|
6
|
+
const recent = engine.recentRuns(routine.id, 10);
|
|
7
|
+
const lastRun = recent[0] ?? null;
|
|
8
|
+
let streakSuccess = 0;
|
|
9
|
+
for (const r of recent) {
|
|
10
|
+
if (r.status === 'ok')
|
|
11
|
+
streakSuccess++;
|
|
12
|
+
else
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
const costAgg = engine.costSinceDays(routine.id, 30);
|
|
16
|
+
return { routine, recent, lastRun, streakSuccess, totalUsd: costAgg.usd };
|
|
17
|
+
}
|
|
18
|
+
function statusColor(status) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case 'ok': return 'green';
|
|
21
|
+
case 'failed': return 'red';
|
|
22
|
+
case 'timeout': return 'yellow';
|
|
23
|
+
case 'aborted': return 'gray';
|
|
24
|
+
case 'running': return 'cyan';
|
|
25
|
+
default: return 'gray';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function ScheduleBadge({ routine }) {
|
|
29
|
+
if (routine.schedule.kind === 'manual') {
|
|
30
|
+
return _jsx(Text, { color: "gray", children: "manual" });
|
|
31
|
+
}
|
|
32
|
+
return _jsx(Text, { color: "cyan", children: routine.schedule.onCalendar });
|
|
33
|
+
}
|
|
34
|
+
function RoutineListRow({ row, selected, }) {
|
|
35
|
+
const { routine, lastRun, streakSuccess } = row;
|
|
36
|
+
const targetCount = routine.targets.length || (routine.perTarget ? 0 : 1);
|
|
37
|
+
const targetLabel = routine.perTarget
|
|
38
|
+
? routine.targets.length > 0 ? `${routine.targets.length}×` : 'all×'
|
|
39
|
+
: 'singleton';
|
|
40
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: selected ? 'cyan' : undefined, children: selected ? '▶' : ' ' }) }), _jsx(Box, { width: 2, children: routine.enabled ? _jsx(Text, { color: "green", children: "\u25CF" }) : _jsx(Text, { color: "gray", children: "\u25CB" }) }), _jsx(Box, { width: 22, children: _jsx(Text, { bold: selected, children: truncate(routine.id, 20) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: _jsx(Text, { color: statusColor(lastRun?.status ?? null), children: lastRun?.status ?? '—' }) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { children: streakSuccess > 0 ? _jsxs(Text, { color: "green", children: ["\u00D7", streakSuccess] }) : _jsx(Text, { color: "gray", children: "\u2014" }) }) }), _jsx(Box, { width: 16, children: _jsx(Text, { color: "gray", children: formatRelative(lastRun?.startedAt ?? null) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: "gray", children: formatDuration(lastRun?.durationMs ?? null) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: "gray", children: formatUsd(row.totalUsd) }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: "gray", children: targetLabel }) }), _jsx(ScheduleBadge, { routine: routine })] }));
|
|
41
|
+
}
|
|
42
|
+
function RoutineListHeader() {
|
|
43
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 2, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: 2, children: _jsx(Text, { bold: true, children: "ON" }) }), _jsx(Box, { width: 22, children: _jsx(Text, { bold: true, children: "ID" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { bold: true, children: "LAST" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "STREAK" }) }), _jsx(Box, { width: 16, children: _jsx(Text, { bold: true, children: "WHEN" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "DUR" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "30d $" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "TARGETS" }) }), _jsx(Text, { bold: true, children: "SCHEDULE" })] }));
|
|
44
|
+
}
|
|
45
|
+
function RecentRunsPanel({ runs }) {
|
|
46
|
+
if (runs.length === 0)
|
|
47
|
+
return _jsx(Text, { color: "gray", children: " no runs yet" });
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: 22, children: _jsx(Text, { bold: true, children: "WHEN" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "STATUS" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "DUR" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "EXIT" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, children: "USD" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "TOKENS" }) }), _jsx(Text, { bold: true, children: "ERROR" })] }), runs.map(r => (_jsxs(Box, { children: [_jsx(Box, { width: 22, children: _jsx(Text, { color: "gray", children: formatRelative(r.startedAt) }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: statusColor(r.status), children: r.status }) }), _jsx(Box, { width: 10, children: _jsx(Text, { children: formatDuration(r.durationMs) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { children: r.exitCode ?? '—' }) }), _jsx(Box, { width: 10, children: _jsx(Text, { children: formatUsd(r.usd) }) }), _jsx(Box, { width: 12, children: _jsx(Text, { children: r.inputTokens != null ? `${(r.inputTokens + (r.outputTokens ?? 0)).toLocaleString()}` : '—' }) }), _jsx(Text, { color: "red", children: truncate(r.error ?? '', 40) })] }, r.runId)))] }));
|
|
49
|
+
}
|
|
50
|
+
function RoutineDetail({ row }) {
|
|
51
|
+
const { routine, recent } = row;
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, gap: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: routine.id }), _jsx(Text, { children: routine.name }), routine.description && _jsx(Text, { color: "gray", children: routine.description })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Schedule" }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " kind: " }), _jsx(Text, { children: routine.schedule.kind })] }), routine.schedule.kind === 'calendar' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " when: " }), _jsx(Text, { children: routine.schedule.onCalendar })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " jitter: " }), _jsxs(Text, { children: [routine.schedule.randomizedDelaySec, "s"] })] })] }))] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Task" }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " runner: " }), _jsx(Text, { children: routine.task.kind })] }), routine.task.kind === 'claude-cli' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " tokens: " }), _jsx(Text, { children: routine.task.tokenCap.toLocaleString() })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " max USD: " }), _jsx(Text, { children: formatUsd(routine.task.maxUsd) })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " timeout: " }), _jsx(Text, { children: formatDuration(routine.task.wallClockMs) })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " prompt: " }), _jsx(Text, { children: truncate(routine.task.prompt, 80) })] })] })), routine.task.kind === 'shell' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " argv: " }), _jsx(Text, { children: truncate(routine.task.argv.join(' '), 80) })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " timeout: " }), _jsx(Text, { children: formatDuration(routine.task.wallClockMs) })] })] })), routine.task.kind === 'mcp-call' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " tool: " }), _jsx(Text, { children: routine.task.tool })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " timeout: " }), _jsx(Text, { children: formatDuration(routine.task.wallClockMs) })] })] }))] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent runs" }), _jsx(RecentRunsPanel, { runs: recent })] })] }));
|
|
53
|
+
}
|
|
54
|
+
export function RoutinesTab({ engine, routines, selectedIndex, detailOpen, }) {
|
|
55
|
+
const rows = useMemo(() => routines.map(r => summarise(engine, r)), [engine, routines]);
|
|
56
|
+
const selected = rows[selectedIndex];
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, children: ["Routines (", rows.length, ")"] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(RoutineListHeader, {}), rows.length === 0 && _jsx(Text, { color: "gray", children: " no routines yet" }), rows.map((row, i) => (_jsx(RoutineListRow, { row: row, selected: i === selectedIndex }, row.routine.id)))] }), detailOpen && selected && _jsx(RoutineDetail, { row: selected }), !detailOpen && selected && (_jsx(Text, { color: "gray", children: "press Enter for detail \u00B7 r run now \u00B7 e toggle enabled" }))] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
6
|
+
const FIELDS = [
|
|
7
|
+
{ id: 'name', label: 'app name (kebab-case)' },
|
|
8
|
+
{ id: 'composePath', label: 'compose path' },
|
|
9
|
+
{ id: 'port', label: 'public port' },
|
|
10
|
+
{ id: 'domain', label: 'primary domain' },
|
|
11
|
+
{ id: 'usesSharedDb', label: 'joins databases network?', toggle: true },
|
|
12
|
+
{ id: 'nonRootUser', label: 'Dockerfile USER non-root?', toggle: true },
|
|
13
|
+
];
|
|
14
|
+
function buildPlan(draft) {
|
|
15
|
+
const errors = [];
|
|
16
|
+
if (!/^[a-z][a-z0-9-]{1,62}$/.test(draft.name))
|
|
17
|
+
errors.push('app name: lowercase + dashes only');
|
|
18
|
+
if (!draft.composePath.startsWith('/'))
|
|
19
|
+
errors.push('compose path: absolute');
|
|
20
|
+
if (!/^\d{2,5}$/.test(draft.port))
|
|
21
|
+
errors.push('port: 2–5 digit integer');
|
|
22
|
+
if (errors.length > 0)
|
|
23
|
+
return { ok: false, errors, commands: [] };
|
|
24
|
+
const unit = `/etc/systemd/system/${draft.name}.service`;
|
|
25
|
+
const commands = [
|
|
26
|
+
`# 1. Scaffold the systemd unit`,
|
|
27
|
+
`sudo tee ${unit} > /dev/null <<'UNIT'`,
|
|
28
|
+
`[Unit]`,
|
|
29
|
+
`Description=${draft.name} Docker Service`,
|
|
30
|
+
`Requires=docker.service`,
|
|
31
|
+
`After=docker.service network-online.target${draft.usesSharedDb ? ' docker-databases.service' : ''}`,
|
|
32
|
+
`Wants=network-online.target`,
|
|
33
|
+
draft.usesSharedDb ? `Requires=docker-databases.service` : '',
|
|
34
|
+
``,
|
|
35
|
+
`[Service]`,
|
|
36
|
+
`Type=oneshot`,
|
|
37
|
+
`RemainAfterExit=yes`,
|
|
38
|
+
`WorkingDirectory=${draft.composePath}`,
|
|
39
|
+
`ExecStartPre=-/usr/bin/docker compose down`,
|
|
40
|
+
`ExecStart=/usr/bin/docker compose up -d --force-recreate`,
|
|
41
|
+
`ExecStop=/usr/bin/docker compose down`,
|
|
42
|
+
`ExecReload=/usr/bin/docker compose restart`,
|
|
43
|
+
`TimeoutStartSec=300`,
|
|
44
|
+
`Restart=on-failure`,
|
|
45
|
+
`RestartSec=10`,
|
|
46
|
+
``,
|
|
47
|
+
`[Install]`,
|
|
48
|
+
`WantedBy=multi-user.target`,
|
|
49
|
+
`UNIT`,
|
|
50
|
+
``,
|
|
51
|
+
`# 2. Daemon-reload + enable`,
|
|
52
|
+
`sudo systemctl daemon-reload`,
|
|
53
|
+
`sudo systemctl enable --now ${draft.name}`,
|
|
54
|
+
``,
|
|
55
|
+
`# 3. Register with fleet (adds to registry.json + detects compose/ports)`,
|
|
56
|
+
`fleet add ${draft.composePath}`,
|
|
57
|
+
``,
|
|
58
|
+
`# 4. Nginx reverse proxy for ${draft.domain}`,
|
|
59
|
+
`fleet nginx add ${draft.domain} --port ${draft.port} --type spa`,
|
|
60
|
+
];
|
|
61
|
+
if (draft.usesSharedDb) {
|
|
62
|
+
commands.push('', `# 5. Ensure databases network is reachable`, `docker network inspect databases > /dev/null || docker network create databases`);
|
|
63
|
+
}
|
|
64
|
+
if (draft.nonRootUser) {
|
|
65
|
+
commands.push('', `# 6. Guardian whitelist check (/runc must be whitelisted for non-root containers)`, `grep -q '^/runc$' /etc/guardian/whitelist || echo '/runc' | sudo tee -a /etc/guardian/whitelist`, `sudo systemctl reload guardiand || true`);
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, errors: [], commands: commands.filter(c => c !== undefined) };
|
|
68
|
+
}
|
|
69
|
+
export function ScaffoldTab() {
|
|
70
|
+
const [draft, setDraft] = useState({
|
|
71
|
+
name: '',
|
|
72
|
+
composePath: '/home/matt/',
|
|
73
|
+
port: '3000',
|
|
74
|
+
domain: '',
|
|
75
|
+
usesSharedDb: true,
|
|
76
|
+
nonRootUser: false,
|
|
77
|
+
});
|
|
78
|
+
const [cursor, setCursor] = useState(0);
|
|
79
|
+
const [editing, setEditing] = useState(false);
|
|
80
|
+
const [textValue, setTextValue] = useState(() => String(draft[FIELDS[0].id]));
|
|
81
|
+
const [plan, setPlan] = useState(null);
|
|
82
|
+
const currentField = FIELDS[cursor];
|
|
83
|
+
useRegisterHandler((input, key) => {
|
|
84
|
+
if (editing && !currentField.toggle)
|
|
85
|
+
return false;
|
|
86
|
+
if (input === 'g') {
|
|
87
|
+
setPlan(buildPlan(draft));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (currentField.toggle && (input === ' ' || key.return)) {
|
|
91
|
+
setDraft(d => ({ ...d, [currentField.id]: !d[currentField.id] }));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (input === 'j' || key.downArrow) {
|
|
95
|
+
const next = Math.min(cursor + 1, FIELDS.length - 1);
|
|
96
|
+
setCursor(next);
|
|
97
|
+
if (!FIELDS[next].toggle)
|
|
98
|
+
setTextValue(String(draft[FIELDS[next].id] ?? ''));
|
|
99
|
+
setEditing(false);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (input === 'k' || key.upArrow) {
|
|
103
|
+
const next = Math.max(cursor - 1, 0);
|
|
104
|
+
setCursor(next);
|
|
105
|
+
if (!FIELDS[next].toggle)
|
|
106
|
+
setTextValue(String(draft[FIELDS[next].id] ?? ''));
|
|
107
|
+
setEditing(false);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (input === 'e' && !currentField.toggle) {
|
|
111
|
+
setEditing(true);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "New app scaffold" }), _jsx(Text, { color: "gray", children: "answer the prompts, press `g` to generate the deployment commands. Nothing is applied automatically." }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: FIELDS.map((f, i) => {
|
|
117
|
+
const selected = i === cursor;
|
|
118
|
+
const value = draft[f.id];
|
|
119
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: selected ? 'cyan' : undefined, children: selected ? '▶' : ' ' }) }), _jsx(Box, { width: 32, children: _jsx(Text, { color: selected ? 'cyan' : 'gray', children: f.label }) }), f.toggle ? (_jsx(Text, { color: value ? 'green' : 'gray', children: value ? 'yes' : 'no' })) : editing && selected ? (_jsx(TextInput, { value: textValue, onChange: setTextValue, onSubmit: () => {
|
|
120
|
+
setDraft(prev => ({ ...prev, [f.id]: textValue }));
|
|
121
|
+
setEditing(false);
|
|
122
|
+
} })) : (_jsx(Text, { children: String(value) }))] }, f.id));
|
|
123
|
+
}) }), _jsx(Text, { color: "gray", children: "j/k move \u00B7 e edit \u00B7 space toggle \u00B7 g generate plan" }), plan && !plan.ok && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { color: "red", bold: true, children: "errors" }), plan.errors.map((e, i) => _jsxs(Text, { color: "red", children: [" \u00B7 ", e] }, i))] })), plan?.ok && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { color: "green", bold: true, children: "deployment commands" }), plan.commands.map((line, i) => {
|
|
124
|
+
const isComment = line.startsWith('#');
|
|
125
|
+
return (_jsx(Text, { color: isComment ? 'gray' : undefined, children: line }, i));
|
|
126
|
+
})] }))] }));
|
|
127
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { formatRelative, truncate } from '../../../tui/routines/format.js';
|
|
5
|
+
import { useSecurity } from '../../../tui/routines/hooks/use-security.js';
|
|
6
|
+
function certColor(c) {
|
|
7
|
+
if (c.daysUntil == null)
|
|
8
|
+
return 'gray';
|
|
9
|
+
if (c.daysUntil <= 7)
|
|
10
|
+
return 'red';
|
|
11
|
+
if (c.daysUntil <= 30)
|
|
12
|
+
return 'yellow';
|
|
13
|
+
return 'green';
|
|
14
|
+
}
|
|
15
|
+
function ageColor(days) {
|
|
16
|
+
if (days == null)
|
|
17
|
+
return 'gray';
|
|
18
|
+
if (days >= 180)
|
|
19
|
+
return 'red';
|
|
20
|
+
if (days >= 90)
|
|
21
|
+
return 'yellow';
|
|
22
|
+
return 'green';
|
|
23
|
+
}
|
|
24
|
+
export function SecurityTab({ apps }) {
|
|
25
|
+
const snap = useSecurity(apps);
|
|
26
|
+
const expiringSoon = snap.certs.filter(c => c.daysUntil != null && c.daysUntil <= 30).length;
|
|
27
|
+
const rotationsDue = snap.secretAges.filter(s => s.ageDays != null && s.ageDays >= 90).length;
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Security overview" }), _jsxs(Text, { color: expiringSoon > 0 ? 'yellow' : 'green', children: [expiringSoon, " certs expiring \u226430d"] }), _jsxs(Text, { color: rotationsDue > 0 ? 'yellow' : 'green', children: [rotationsDue, " secrets overdue for rotation"] }), snap.loading ? (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " refreshing"] })) : (_jsxs(Text, { color: "gray", children: ["updated ", formatRelative(new Date(snap.refreshedAt).toISOString())] }))] }), _jsxs(Box, { flexDirection: "row", gap: 4, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 48, children: [_jsx(Text, { bold: true, children: "Guardian" }), !snap.guardian ? (_jsx(Text, { color: "gray", children: " \u2014" })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " binary" }) }), _jsx(Text, { color: snap.guardian.binaryInstalled ? 'green' : 'red', children: snap.guardian.binaryInstalled ? 'installed' : 'missing' })] }), _jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " whitelist" }) }), _jsx(Text, { color: snap.guardian.whitelistExists ? 'green' : 'red', children: snap.guardian.whitelistExists
|
|
29
|
+
? `${snap.guardian.whitelistLines ?? '?'} entries`
|
|
30
|
+
: 'missing' })] }), _jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " /runc whitelisted" }) }), _jsx(Text, { color: snap.guardian.runcWhitelisted ? 'green' : 'red', children: snap.guardian.runcWhitelisted ? 'yes' : snap.guardian.runcWhitelisted === false ? 'NO — containers at risk' : '—' })] })] }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 48, children: [_jsx(Text, { bold: true, children: "SSH agent" }), !snap.ssh ? (_jsx(Text, { color: "gray", children: " \u2014" })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " socket" }) }), _jsx(Text, { color: snap.ssh.socketExists ? 'green' : 'red', children: snap.ssh.socketExists ? '/tmp/fleet-ssh-agent.sock' : 'not present' })] }), _jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " key loaded" }) }), _jsx(Text, { color: snap.ssh.keyLoaded ? 'green' : 'red', children: snap.ssh.keyLoaded ? 'yes' : snap.ssh.keyLoaded === false ? 'NO — git push will fail' : '—' })] }), snap.ssh.keyFingerprint && (_jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: " fingerprint" }) }), _jsx(Text, { children: truncate(snap.ssh.keyFingerprint, 28) })] }))] }))] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["TLS certificates (", snap.certs.length, ")"] }), snap.certs.length === 0 && _jsx(Text, { color: "gray", children: " no domains to check" }), snap.certs.slice(0, 10).map(c => (_jsxs(Box, { children: [_jsx(Box, { width: 32, children: _jsx(Text, { children: truncate(c.domain, 30) }) }), _jsx(Box, { width: 16, children: _jsx(Text, { color: certColor(c), children: c.daysUntil == null ? 'no cert found' : `${c.daysUntil}d` }) }), _jsx(Text, { color: "gray", children: c.expiresAt ? formatRelative(c.expiresAt) : '' })] }, c.domain))), snap.certs.length > 10 && _jsxs(Text, { color: "gray", children: [" +", snap.certs.length - 10, " more\u2026"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Secret rotation age (", snap.secretAges.length, ")"] }), snap.secretAges.length === 0 && _jsx(Text, { color: "gray", children: " no managed secrets" }), snap.secretAges.slice(0, 10).map(s => (_jsxs(Box, { children: [_jsx(Box, { width: 22, children: _jsx(Text, { children: truncate(s.app, 20) }) }), _jsx(Box, { width: 16, children: _jsx(Text, { color: ageColor(s.ageDays), children: s.ageDays != null ? `${s.ageDays}d old` : (s.error ?? '—') }) })] }, s.app)))] })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { dbPath } from '../../../core/routines/db.js';
|
|
4
|
+
const KEY_GROUPS = [
|
|
5
|
+
{
|
|
6
|
+
title: 'Navigation',
|
|
7
|
+
bindings: [
|
|
8
|
+
{ key: '1..8', label: 'jump to numbered tab' },
|
|
9
|
+
{ key: 'j / k or ↓ / ↑', label: 'move cursor' },
|
|
10
|
+
{ key: 'Enter', label: 'drill in / select' },
|
|
11
|
+
{ key: 'Esc', label: 'back / cancel modal' },
|
|
12
|
+
{ key: 'p or Ctrl+K', label: 'command palette' },
|
|
13
|
+
{ key: 'q', label: 'quit' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Routines',
|
|
18
|
+
bindings: [
|
|
19
|
+
{ key: 'n', label: 'new routine' },
|
|
20
|
+
{ key: 'e', label: 'edit selected' },
|
|
21
|
+
{ key: 'd', label: 'delete selected (y/n confirm)' },
|
|
22
|
+
{ key: 't', label: 'toggle enabled' },
|
|
23
|
+
{ key: 'r', label: 'run now (opens live panel)' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: 'Dashboard / Ops',
|
|
28
|
+
bindings: [
|
|
29
|
+
{ key: 'r', label: 'force refresh signals' },
|
|
30
|
+
{ key: 'Enter', label: 'drill into repo detail' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: 'Logs',
|
|
35
|
+
bindings: [
|
|
36
|
+
{ key: 'j / k', label: 'pick service' },
|
|
37
|
+
{ key: 'Enter', label: 'tail selected' },
|
|
38
|
+
{ key: 'w', label: 'toggle warn filter' },
|
|
39
|
+
{ key: 'x', label: 'toggle error filter' },
|
|
40
|
+
{ key: 'c', label: 'clear filter' },
|
|
41
|
+
{ key: 'Esc', label: 'stop tail' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Live-run panel',
|
|
46
|
+
bindings: [
|
|
47
|
+
{ key: 'a', label: 'abort running task' },
|
|
48
|
+
{ key: 'Esc / Enter / q', label: 'close after end' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
function Row({ label, value, color }) {
|
|
53
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 26, children: _jsxs(Text, { color: "gray", children: [" ", label] }) }), _jsx(Text, { color: color, children: value })] }));
|
|
54
|
+
}
|
|
55
|
+
export function SettingsTab({ runtime }) {
|
|
56
|
+
const routinesCount = runtime.store.list().length;
|
|
57
|
+
const enabledCount = runtime.store.list().filter(r => r.enabled).length;
|
|
58
|
+
const storePath = runtime.store.storePath();
|
|
59
|
+
const databasePath = dbPath();
|
|
60
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Settings & reference" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Runtime" }), _jsx(Row, { label: "fleet version", value: "1.4.0" }), _jsx(Row, { label: "ink", value: "5.2.1" }), _jsx(Row, { label: "react", value: "18.3.1" }), _jsx(Row, { label: "routines loaded", value: `${routinesCount} (${enabledCount} enabled)` }), _jsx(Row, { label: "defaults seeded", value: runtime.seeded.seeded > 0 ? `${runtime.seeded.seeded} new` : 'already in place', color: runtime.seeded.seeded > 0 ? 'magenta' : 'gray' })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Paths" }), _jsx(Row, { label: "routines.json", value: storePath }), _jsx(Row, { label: "fleet.db", value: databasePath }), _jsx(Row, { label: "unit template dir", value: "/etc/systemd/system" }), _jsx(Row, { label: "mutex / config dir", value: "/var/lib/fleet/locks \u00B7 /var/lib/fleet/claude-configs" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Adapters enabled" }), _jsx(Row, { label: "scheduler", value: "systemd-timer", color: "green" }), _jsx(Row, { label: "runners", value: "shell \u00B7 claude-cli \u00B7 mcp-call", color: "green" }), _jsx(Row, { label: "notifiers", value: "stdout", color: "green" }), _jsx(Row, { label: "signals", value: "git-clean \u00B7 container-up \u00B7 ci-status", color: "green" })] }), KEY_GROUPS.map(group => (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: group.title }), group.bindings.map(b => (_jsxs(Box, { children: [_jsx(Box, { width: 26, children: _jsxs(Text, { color: "cyan", children: [" ", b.key] }) }), _jsx(Text, { color: "gray", children: b.label })] }, b.key)))] }, group.title)))] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { RoutineEngine } from '../../../core/routines/engine.js';
|
|
3
|
+
export interface TimelineTabProps {
|
|
4
|
+
engine: RoutineEngine;
|
|
5
|
+
sinceDays?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function TimelineTab({ engine, sinceDays }: TimelineTabProps): React.JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
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 { Timeline } from '@matthesketh/ink-timeline';
|
|
5
|
+
import { loadIncidents } from '../../../core/routines/incidents.js';
|
|
6
|
+
import { formatRelative, truncate } from '../../../tui/routines/format.js';
|
|
7
|
+
const KIND_META = {
|
|
8
|
+
'routine-failed': { typeLabel: 'FAIL', typeColor: 'red' },
|
|
9
|
+
'routine-timeout': { typeLabel: 'TIMEOUT', typeColor: 'yellow' },
|
|
10
|
+
'signal-error': { typeLabel: 'ERROR', typeColor: 'red' },
|
|
11
|
+
'signal-warn': { typeLabel: 'WARN', typeColor: 'yellow' },
|
|
12
|
+
};
|
|
13
|
+
export function TimelineTab({ engine, sinceDays = 7 }) {
|
|
14
|
+
const incidents = useMemo(() => loadIncidents(engine.db, { sinceDays, limit: 50 }), [engine.db, sinceDays]);
|
|
15
|
+
const events = incidents.map(i => ({
|
|
16
|
+
time: new Date(i.at),
|
|
17
|
+
type: KIND_META[i.kind].typeLabel,
|
|
18
|
+
typeColor: KIND_META[i.kind].typeColor,
|
|
19
|
+
title: i.subject,
|
|
20
|
+
description: truncate(i.detail || '—', 100),
|
|
21
|
+
}));
|
|
22
|
+
const failCount = incidents.filter(i => i.kind === 'routine-failed' || i.kind === 'routine-timeout').length;
|
|
23
|
+
const warnCount = incidents.filter(i => i.kind === 'signal-warn').length;
|
|
24
|
+
const errCount = incidents.filter(i => i.kind === 'signal-error').length;
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Incident timeline" }), _jsxs(Text, { color: "gray", children: ["last ", sinceDays, "d"] }), _jsxs(Text, { color: "red", children: [failCount, " routine failures"] }), _jsxs(Text, { color: "red", children: [errCount, " signal errors"] }), _jsxs(Text, { color: "yellow", children: [warnCount, " signal warns"] })] }), events.length === 0 ? (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, children: _jsx(Text, { color: "green", children: " nothing to report \u2014 all clear" }) })) : (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Timeline, { events: events, maxVisible: 20, showRelativeTime: true }) })), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Raw stream" }), incidents.slice(0, 20).map((i, idx) => (_jsxs(Box, { children: [_jsx(Box, { width: 18, children: _jsx(Text, { color: "gray", children: formatRelative(i.at) }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: KIND_META[i.kind].typeColor, children: KIND_META[i.kind].typeLabel }) }), _jsx(Box, { width: 30, children: _jsx(Text, { children: truncate(i.subject, 28) }) }), _jsx(Text, { color: "gray", children: truncate(i.detail, 60) })] }, idx)))] })] }));
|
|
26
|
+
}
|
package/dist/tui/state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
|
-
const TOP_VIEWS = ['dashboard', 'health', 'secrets'];
|
|
2
|
+
const TOP_VIEWS = ['dashboard', 'health', 'secrets', 'logs-multi'];
|
|
3
3
|
export const initialState = {
|
|
4
4
|
currentView: 'dashboard',
|
|
5
5
|
previousView: null,
|
|
@@ -9,6 +9,11 @@ export const initialState = {
|
|
|
9
9
|
loading: false,
|
|
10
10
|
error: null,
|
|
11
11
|
confirmAction: null,
|
|
12
|
+
dashboardIndex: 0,
|
|
13
|
+
healthIndex: 0,
|
|
14
|
+
secretsIndex: 0,
|
|
15
|
+
secretsSubView: 'app-list',
|
|
16
|
+
appDetailIndex: 0,
|
|
12
17
|
};
|
|
13
18
|
export function reducer(state, action) {
|
|
14
19
|
switch (action.type) {
|
|
@@ -26,6 +31,7 @@ export function reducer(state, action) {
|
|
|
26
31
|
currentView: state.previousView ?? 'dashboard',
|
|
27
32
|
previousView: null,
|
|
28
33
|
selectedSecret: null,
|
|
34
|
+
secretsSubView: 'app-list',
|
|
29
35
|
error: null,
|
|
30
36
|
confirmAction: null,
|
|
31
37
|
};
|
|
@@ -43,6 +49,15 @@ export function reducer(state, action) {
|
|
|
43
49
|
return { ...state, confirmAction: action.action };
|
|
44
50
|
case 'CANCEL_CONFIRM':
|
|
45
51
|
return { ...state, confirmAction: null };
|
|
52
|
+
case 'SET_INDEX': {
|
|
53
|
+
const key = `${action.view}Index`;
|
|
54
|
+
if (key in state) {
|
|
55
|
+
return { ...state, [key]: action.index };
|
|
56
|
+
}
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
case 'SET_SECRETS_SUBVIEW':
|
|
60
|
+
return { ...state, secretsSubView: action.subView, secretsIndex: 0 };
|
|
46
61
|
default:
|
|
47
62
|
return state;
|
|
48
63
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|