@flame0510/project-aether 1.1.9
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 +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SOUL.md — Prometheus
|
|
2
|
+
|
|
3
|
+
You are **Prometheus** 🔥, a project management agent.
|
|
4
|
+
|
|
5
|
+
## Vision
|
|
6
|
+
Bridge the gap between technical execution and client expectations. You manage scope, deadlines, and deliverables.
|
|
7
|
+
|
|
8
|
+
## Core rules
|
|
9
|
+
- **Client first.** Every decision starts with "does this serve the client?"
|
|
10
|
+
- **Scope management.** Know when to say yes, how to say no, and how to escalate.
|
|
11
|
+
- **Delivery > perfection.** Ship on time, iterate afterwards.
|
|
12
|
+
- **Documentation.** Written specs prevent misunderstandings.
|
|
13
|
+
|
|
14
|
+
## Method
|
|
15
|
+
1. Clarify requirements before acting.
|
|
16
|
+
2. Break projects into deliverables with clear acceptance criteria.
|
|
17
|
+
3. Communicate progress before being asked.
|
|
18
|
+
4. Learn from every project — capture what worked and what didn't.
|
|
19
|
+
|
|
20
|
+
## Tone
|
|
21
|
+
Professional, clear, proactive. Clients trust you.
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { Button, Modal, Input, Badge } from '../components/ui';
|
|
5
|
+
import { SkeletonLines } from '../components/Skeleton';
|
|
6
|
+
|
|
7
|
+
interface TelegramChannel {
|
|
8
|
+
connected: boolean;
|
|
9
|
+
botToken: string;
|
|
10
|
+
dmPolicy: string;
|
|
11
|
+
allowFrom: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PendingPairing {
|
|
15
|
+
code: string;
|
|
16
|
+
senderId: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ApprovedPairing {
|
|
20
|
+
senderId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChannelsData {
|
|
24
|
+
agentId: string;
|
|
25
|
+
telegram: TelegramChannel | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
agentId: string;
|
|
30
|
+
agentName: string;
|
|
31
|
+
open: boolean;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PAIRING_POLL_MS = 5000;
|
|
36
|
+
|
|
37
|
+
export default function ChannelManager({ agentId, agentName, open, onClose }: Props) {
|
|
38
|
+
const [channels, setChannels] = useState<ChannelsData | null>(null);
|
|
39
|
+
const [channelsLoading, setChannelsLoading] = useState(true);
|
|
40
|
+
const [pairingsLoading, setPairingsLoading] = useState(true);
|
|
41
|
+
const [loading, setLoading] = useState(false);
|
|
42
|
+
const [disconnecting, setDisconnecting] = useState(false);
|
|
43
|
+
const [approvingCode, setApprovingCode] = useState<string | null>(null);
|
|
44
|
+
const [revokingId, setRevokingId] = useState<string | null>(null);
|
|
45
|
+
const [error, setError] = useState('');
|
|
46
|
+
const [success, setSuccess] = useState('');
|
|
47
|
+
|
|
48
|
+
const [tgToken, setTgToken] = useState('');
|
|
49
|
+
const [restarting, setRestarting] = useState(false);
|
|
50
|
+
const [confirmAction, setConfirmAction] = useState<{ type: 'revoke'; channel: string; senderId: string } | { type: 'disconnect'; channel: string } | null>(null);
|
|
51
|
+
|
|
52
|
+
const [pending, setPending] = useState<PendingPairing[]>([]);
|
|
53
|
+
const [approved, setApproved] = useState<ApprovedPairing[]>([]);
|
|
54
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
55
|
+
const pairingsAbortRef = useRef<AbortController | null>(null);
|
|
56
|
+
const channelsAbortRef = useRef<AbortController | null>(null);
|
|
57
|
+
|
|
58
|
+
const fetchChannels = useCallback(async () => {
|
|
59
|
+
// Abort any in-flight channels request
|
|
60
|
+
channelsAbortRef.current?.abort();
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
channelsAbortRef.current = controller;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels`, { signal: controller.signal });
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
setChannels(data);
|
|
68
|
+
}
|
|
69
|
+
} catch (e: any) {
|
|
70
|
+
if (e?.name !== 'AbortError') { /* ignore aborted */ }
|
|
71
|
+
}
|
|
72
|
+
finally { setChannelsLoading(false); }
|
|
73
|
+
}, [agentId]);
|
|
74
|
+
|
|
75
|
+
// Shared fetch helper for pairings — cancels previous in-flight request
|
|
76
|
+
const doFetchPairings = useCallback(async (showLoading: boolean) => {
|
|
77
|
+
pairingsAbortRef.current?.abort();
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
pairingsAbortRef.current = controller;
|
|
80
|
+
if (showLoading) setPairingsLoading(true);
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/pairing?channel=telegram`, { signal: controller.signal });
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
setPending(data.pending ?? []);
|
|
86
|
+
setApproved(data.approved ?? []);
|
|
87
|
+
}
|
|
88
|
+
} catch (e: any) {
|
|
89
|
+
if (e?.name !== 'AbortError') { /* ignore aborted */ }
|
|
90
|
+
}
|
|
91
|
+
finally { if (showLoading) setPairingsLoading(false); }
|
|
92
|
+
}, [agentId]);
|
|
93
|
+
|
|
94
|
+
const fetchPairings = useCallback(() => doFetchPairings(true), [doFetchPairings]);
|
|
95
|
+
const pollPairings = useCallback(() => doFetchPairings(false), [doFetchPairings]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (open) {
|
|
99
|
+
setChannelsLoading(true);
|
|
100
|
+
setPairingsLoading(true);
|
|
101
|
+
setChannels(null);
|
|
102
|
+
setPending([]);
|
|
103
|
+
setApproved([]);
|
|
104
|
+
fetchChannels();
|
|
105
|
+
setError('');
|
|
106
|
+
setSuccess('');
|
|
107
|
+
setTgToken('');
|
|
108
|
+
}
|
|
109
|
+
return () => {
|
|
110
|
+
if (pollRef.current) clearInterval(pollRef.current);
|
|
111
|
+
channelsAbortRef.current?.abort();
|
|
112
|
+
pairingsAbortRef.current?.abort();
|
|
113
|
+
};
|
|
114
|
+
}, [open, fetchChannels]);
|
|
115
|
+
|
|
116
|
+
// Initial pairings fetch when channels load
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!channelsLoading && channels?.telegram?.connected) {
|
|
119
|
+
fetchPairings();
|
|
120
|
+
}
|
|
121
|
+
}, [channelsLoading, channels?.telegram?.connected, fetchPairings]);
|
|
122
|
+
|
|
123
|
+
// Poll pairings when on the active tab and connected/linked
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (open && channels?.telegram?.connected) {
|
|
126
|
+
pollRef.current = setInterval(pollPairings, PAIRING_POLL_MS);
|
|
127
|
+
} else {
|
|
128
|
+
if (pollRef.current) clearInterval(pollRef.current);
|
|
129
|
+
}
|
|
130
|
+
return () => { if (pollRef.current) clearInterval(pollRef.current); };
|
|
131
|
+
}, [open, channels?.telegram?.connected, pollPairings]);
|
|
132
|
+
|
|
133
|
+
const handleTelegramConnect = async () => {
|
|
134
|
+
setLoading(true); setError(''); setSuccess('');
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/telegram`, {
|
|
137
|
+
method: 'PUT',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ botToken: tgToken }),
|
|
140
|
+
});
|
|
141
|
+
const data = await res.json();
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
setError(data.error || 'Failed to connect Telegram');
|
|
144
|
+
} else {
|
|
145
|
+
setSuccess('Token saved. Restart the agent to activate the bot.');
|
|
146
|
+
setTgToken('');
|
|
147
|
+
setPairingsLoading(true);
|
|
148
|
+
fetchChannels();
|
|
149
|
+
}
|
|
150
|
+
} catch { setError('Network error'); }
|
|
151
|
+
finally { setLoading(false); }
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleTelegramDisconnect = async () => {
|
|
155
|
+
setConfirmAction({ type: 'disconnect', channel: 'telegram' });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const executeDisconnect = async (_channel: string) => {
|
|
159
|
+
setConfirmAction(null);
|
|
160
|
+
setDisconnecting(true); setError(''); setSuccess('');
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/telegram`, { method: 'DELETE' });
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const data = await res.json().catch(() => ({ error: 'Failed' }));
|
|
165
|
+
setError(data.error || 'Failed to disconnect Telegram');
|
|
166
|
+
} else {
|
|
167
|
+
setSuccess('Telegram disconnected.');
|
|
168
|
+
setPending([]);
|
|
169
|
+
fetchChannels();
|
|
170
|
+
}
|
|
171
|
+
} catch { setError('Network error'); }
|
|
172
|
+
finally { setDisconnecting(false); }
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleRestartAgent = async () => {
|
|
176
|
+
setRestarting(true); setError(''); setSuccess('');
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/restart`, { method: 'POST' });
|
|
179
|
+
const data = await res.json();
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
setError(data.error || 'Failed to restart agent');
|
|
182
|
+
} else {
|
|
183
|
+
setSuccess('Agent restarted. The bot is now active. Send /start on Telegram to get a pairing code.');
|
|
184
|
+
fetchChannels();
|
|
185
|
+
}
|
|
186
|
+
} catch { setError('Network error'); }
|
|
187
|
+
finally { setRestarting(false); }
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleApprovePairing = async (code: string) => {
|
|
191
|
+
setApprovingCode(code); setError(''); setSuccess('');
|
|
192
|
+
try {
|
|
193
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/pairing?channel=telegram`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ code }),
|
|
197
|
+
});
|
|
198
|
+
const data = await res.json();
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
setError(data.error || 'Failed to approve');
|
|
201
|
+
} else {
|
|
202
|
+
setSuccess(`Pairing ${code} approved.`);
|
|
203
|
+
await doFetchPairings(false);
|
|
204
|
+
}
|
|
205
|
+
} catch { setError('Network error'); }
|
|
206
|
+
finally { setApprovingCode(null); }
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleRevokePairing = async (senderId: string) => {
|
|
210
|
+
setConfirmAction({ type: 'revoke', channel: 'telegram', senderId });
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const executeRevoke = async (senderId: string, _channel: string) => {
|
|
214
|
+
setConfirmAction(null);
|
|
215
|
+
setRevokingId(senderId); setError(''); setSuccess('');
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetch(
|
|
218
|
+
`/api/agents/${encodeURIComponent(agentId)}/channels/pairing?channel=telegram&senderId=${encodeURIComponent(senderId)}`,
|
|
219
|
+
{ method: 'DELETE' },
|
|
220
|
+
);
|
|
221
|
+
const data = await res.json();
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
setError(data.error || 'Failed to revoke');
|
|
224
|
+
} else {
|
|
225
|
+
setSuccess(`Revoked ${senderId}.`);
|
|
226
|
+
await doFetchPairings(false);
|
|
227
|
+
}
|
|
228
|
+
} catch { setError('Network error'); }
|
|
229
|
+
finally { setRevokingId(null); }
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const tg = channels?.telegram;
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Modal open={open} onClose={onClose} title={`Channel Manager: ${agentName}`} maxWidth={520}>
|
|
236
|
+
|
|
237
|
+
{error && <div style={{ margin: '8px 0' }}><Badge tone="danger">{error}</Badge></div>}
|
|
238
|
+
{success && <div style={{ margin: '8px 0' }}><Badge tone="success">{success}</Badge></div>}
|
|
239
|
+
|
|
240
|
+
{/* Confirm modal */}
|
|
241
|
+
<Modal
|
|
242
|
+
open={confirmAction !== null}
|
|
243
|
+
onClose={() => setConfirmAction(null)}
|
|
244
|
+
title="Confirm"
|
|
245
|
+
maxWidth={340}
|
|
246
|
+
tone="warning"
|
|
247
|
+
>
|
|
248
|
+
<p style={{ fontSize: 12, color: 'var(--text)', margin: '0 0 16px 0' }}>
|
|
249
|
+
{confirmAction?.type === 'revoke'
|
|
250
|
+
? `Revoke access for sender ${confirmAction.senderId}?`
|
|
251
|
+
: 'Disconnect Telegram? All paired users will be removed.'}
|
|
252
|
+
</p>
|
|
253
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
254
|
+
<Button
|
|
255
|
+
variant="danger"
|
|
256
|
+
size="sm"
|
|
257
|
+
onClick={() => {
|
|
258
|
+
if (confirmAction?.type === 'revoke') executeRevoke(confirmAction.senderId, confirmAction.channel);
|
|
259
|
+
else if (confirmAction?.type === 'disconnect') executeDisconnect(confirmAction.channel);
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
Confirm
|
|
263
|
+
</Button>
|
|
264
|
+
<Button variant="secondary" size="sm" onClick={() => setConfirmAction(null)}>
|
|
265
|
+
Cancel
|
|
266
|
+
</Button>
|
|
267
|
+
</div>
|
|
268
|
+
</Modal>
|
|
269
|
+
|
|
270
|
+
{channelsLoading ? (
|
|
271
|
+
<SkeletonLines count={4} />
|
|
272
|
+
) : (
|
|
273
|
+
<TelegramTab
|
|
274
|
+
tg={tg ?? null}
|
|
275
|
+
tgToken={tgToken}
|
|
276
|
+
setTgToken={setTgToken}
|
|
277
|
+
loading={loading}
|
|
278
|
+
disconnecting={disconnecting}
|
|
279
|
+
pairingsLoading={pairingsLoading}
|
|
280
|
+
restarting={restarting}
|
|
281
|
+
pending={pending}
|
|
282
|
+
approved={approved}
|
|
283
|
+
approvingCode={approvingCode}
|
|
284
|
+
revokingId={revokingId}
|
|
285
|
+
onConnect={handleTelegramConnect}
|
|
286
|
+
onDisconnect={handleTelegramDisconnect}
|
|
287
|
+
onRestart={handleRestartAgent}
|
|
288
|
+
onApprove={handleApprovePairing}
|
|
289
|
+
onRevoke={handleRevokePairing}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
</Modal>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Telegram Tab ─────────────────────────────
|
|
297
|
+
|
|
298
|
+
function TelegramTab({
|
|
299
|
+
tg, tgToken, setTgToken, loading, disconnecting, pairingsLoading, restarting,
|
|
300
|
+
pending, approved, approvingCode, revokingId,
|
|
301
|
+
onConnect, onDisconnect, onRestart, onApprove, onRevoke,
|
|
302
|
+
}: {
|
|
303
|
+
tg: TelegramChannel | null;
|
|
304
|
+
tgToken: string;
|
|
305
|
+
setTgToken: (v: string) => void;
|
|
306
|
+
loading: boolean;
|
|
307
|
+
disconnecting: boolean;
|
|
308
|
+
pairingsLoading: boolean;
|
|
309
|
+
restarting: boolean;
|
|
310
|
+
pending: PendingPairing[];
|
|
311
|
+
approved: ApprovedPairing[];
|
|
312
|
+
approvingCode: string | null;
|
|
313
|
+
revokingId: string | null;
|
|
314
|
+
onConnect: () => void;
|
|
315
|
+
onDisconnect: () => void;
|
|
316
|
+
onRestart: () => void;
|
|
317
|
+
onApprove: (code: string) => void;
|
|
318
|
+
onRevoke: (senderId: string) => void;
|
|
319
|
+
}) {
|
|
320
|
+
if (tg?.connected) {
|
|
321
|
+
return (
|
|
322
|
+
<div>
|
|
323
|
+
<Badge tone="success">Connected — {tg.botToken} · DM: {tg.dmPolicy}</Badge>
|
|
324
|
+
|
|
325
|
+
<div style={{ marginTop: 12 }}>
|
|
326
|
+
<Button variant="secondary" size="sm" loading={restarting} disabled={restarting} onClick={onRestart}>
|
|
327
|
+
{restarting ? 'Restarting…' : 'Restart Agent'}
|
|
328
|
+
</Button>
|
|
329
|
+
<span style={{ fontSize: 10, color: 'var(--text-dim)', marginLeft: 10 }}>
|
|
330
|
+
Required after token changes
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{/* Pairing */}
|
|
335
|
+
<PairingSection
|
|
336
|
+
pending={pending}
|
|
337
|
+
approved={approved}
|
|
338
|
+
pairingsLoading={pairingsLoading}
|
|
339
|
+
approvingCode={approvingCode}
|
|
340
|
+
revokingId={revokingId}
|
|
341
|
+
onApprove={onApprove}
|
|
342
|
+
onRevoke={onRevoke}
|
|
343
|
+
/>
|
|
344
|
+
|
|
345
|
+
<div style={{ marginTop: 16 }}>
|
|
346
|
+
<Button variant="danger" size="sm" onClick={onDisconnect} loading={disconnecting} disabled={disconnecting}>
|
|
347
|
+
{disconnecting ? 'Disconnecting…' : 'Disconnect Telegram'}
|
|
348
|
+
</Button>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div>
|
|
356
|
+
<p style={{ fontSize: 11, color: 'var(--text-dim)', margin: '0 0 10px' }}>
|
|
357
|
+
Get a bot token from{' '}
|
|
358
|
+
<a href="https://t.me/BotFather" target="_blank" rel="noopener" style={{ color: 'var(--violet)' }}>
|
|
359
|
+
@BotFather
|
|
360
|
+
</a>{' '}
|
|
361
|
+
on Telegram.
|
|
362
|
+
</p>
|
|
363
|
+
|
|
364
|
+
<Input
|
|
365
|
+
label="Bot Token"
|
|
366
|
+
type="password"
|
|
367
|
+
value={tgToken}
|
|
368
|
+
onChange={(e) => setTgToken(e.target.value)}
|
|
369
|
+
placeholder="1234567890:ABCdef..."
|
|
370
|
+
/>
|
|
371
|
+
|
|
372
|
+
<div style={{ marginTop: 12 }}>
|
|
373
|
+
<Button variant="primary" size="sm" onClick={onConnect} loading={loading} disabled={loading || !tgToken.trim()}>
|
|
374
|
+
{loading ? 'Connecting…' : 'Connect Telegram'}
|
|
375
|
+
</Button>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Pairing Section ──────────────────────────
|
|
382
|
+
|
|
383
|
+
function PairingSection({
|
|
384
|
+
pending, approved, pairingsLoading, approvingCode, revokingId,
|
|
385
|
+
onApprove, onRevoke,
|
|
386
|
+
}: {
|
|
387
|
+
pending: PendingPairing[];
|
|
388
|
+
approved: ApprovedPairing[];
|
|
389
|
+
pairingsLoading: boolean;
|
|
390
|
+
approvingCode: string | null;
|
|
391
|
+
revokingId: string | null;
|
|
392
|
+
onApprove: (code: string) => void;
|
|
393
|
+
onRevoke: (senderId: string) => void;
|
|
394
|
+
}) {
|
|
395
|
+
return (
|
|
396
|
+
<div style={{ marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 12 }}>
|
|
397
|
+
<div style={{
|
|
398
|
+
fontSize: 10, color: 'var(--violet)', textTransform: 'uppercase',
|
|
399
|
+
letterSpacing: '0.08em', marginBottom: 8,
|
|
400
|
+
}}>
|
|
401
|
+
Pairing
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<p style={{ fontSize: 11, color: 'var(--text-dim)', margin: '0 0 10px 0' }}>
|
|
405
|
+
Send <code style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--violet)' }}>/start</code> to the bot on Telegram.
|
|
406
|
+
</p>
|
|
407
|
+
|
|
408
|
+
{pairingsLoading ? (
|
|
409
|
+
<SkeletonLines count={3} />
|
|
410
|
+
) : (
|
|
411
|
+
<>
|
|
412
|
+
{/* Pending */}
|
|
413
|
+
{pending.length > 0 && (
|
|
414
|
+
<div style={{ marginBottom: 12 }}>
|
|
415
|
+
<div style={{ fontSize: 10, color: 'var(--yellow)', marginBottom: 6, letterSpacing: '0.04em' }}>PENDING</div>
|
|
416
|
+
{pending.map((p) => (
|
|
417
|
+
<div key={p.code} style={{
|
|
418
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
419
|
+
padding: '6px 0', borderBottom: '1px solid var(--border)',
|
|
420
|
+
}}>
|
|
421
|
+
<code style={{
|
|
422
|
+
fontFamily: 'var(--font-mono)', fontSize: 12,
|
|
423
|
+
color: 'var(--blue)', minWidth: 72,
|
|
424
|
+
}}>{p.code}</code>
|
|
425
|
+
<Button variant="primary" size="sm" onClick={() => onApprove(p.code)} loading={approvingCode === p.code} disabled={approvingCode !== null}>{approvingCode === p.code ? 'Approving…' : 'Approve'}</Button>
|
|
426
|
+
</div>
|
|
427
|
+
))}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
{pending.length === 0 && (
|
|
432
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)', marginBottom: 12 }}>
|
|
433
|
+
No pending requests
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{/* Approved */}
|
|
438
|
+
{approved.length > 0 && (
|
|
439
|
+
<div>
|
|
440
|
+
<div style={{ fontSize: 10, color: 'var(--green)', marginBottom: 6, letterSpacing: '0.04em' }}>APPROVED</div>
|
|
441
|
+
{approved.map((a) => (
|
|
442
|
+
<div key={a.senderId} style={{
|
|
443
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
444
|
+
padding: '6px 0', borderBottom: '1px solid var(--border)',
|
|
445
|
+
fontSize: 11,
|
|
446
|
+
}}>
|
|
447
|
+
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--text)' }}>
|
|
448
|
+
{a.senderId}
|
|
449
|
+
</span>
|
|
450
|
+
<Button variant="danger" size="sm" onClick={() => onRevoke(a.senderId)} loading={revokingId === a.senderId} disabled={revokingId !== null}>{revokingId === a.senderId ? 'Revoking…' : 'Revoke'}</Button>
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{approved.length === 0 && (
|
|
457
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)' }}>
|
|
458
|
+
No approved senders
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { Button } from '../components/ui';
|
|
5
|
+
|
|
6
|
+
interface ImageStatus {
|
|
7
|
+
exists: boolean;
|
|
8
|
+
needsUpdate: boolean;
|
|
9
|
+
downloading: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const POLL_MS = 2000;
|
|
13
|
+
|
|
14
|
+
export default function ImageDownloadBanner({ onImageReady }: { onImageReady?: () => void }) {
|
|
15
|
+
const [status, setStatus] = useState<ImageStatus>({ exists: true, needsUpdate: false, downloading: false });
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
const [justCompleted, setJustCompleted] = useState(false);
|
|
18
|
+
const [actionLoading, setActionLoading] = useState(false);
|
|
19
|
+
const wasDownloadingRef = useRef(false);
|
|
20
|
+
const completedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let active = true;
|
|
24
|
+
|
|
25
|
+
const tick = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch('/api/agents/image-status');
|
|
28
|
+
if (!active) return;
|
|
29
|
+
if (!res.ok) return;
|
|
30
|
+
const data: ImageStatus = await res.json();
|
|
31
|
+
setStatus(data);
|
|
32
|
+
|
|
33
|
+
if (data.downloading) {
|
|
34
|
+
setError(null);
|
|
35
|
+
} else if (wasDownloadingRef.current) {
|
|
36
|
+
setJustCompleted(true);
|
|
37
|
+
setActionLoading(false);
|
|
38
|
+
onImageReady?.();
|
|
39
|
+
if (completedTimerRef.current) clearTimeout(completedTimerRef.current);
|
|
40
|
+
completedTimerRef.current = setTimeout(() => setJustCompleted(false), 3000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
wasDownloadingRef.current = data.downloading;
|
|
44
|
+
} catch { /* ignore */ }
|
|
45
|
+
|
|
46
|
+
if (active) setTimeout(tick, POLL_MS);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
tick();
|
|
50
|
+
return () => { active = false; };
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
return () => {
|
|
55
|
+
if (completedTimerRef.current) clearTimeout(completedTimerRef.current);
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const handleDownload = useCallback(async () => {
|
|
60
|
+
setActionLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch('/api/agents/download-image', { method: 'POST' });
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const data = await res.json().catch(() => ({ error: 'Failed to start download' }));
|
|
66
|
+
setError(data.error || 'Failed to start download');
|
|
67
|
+
setActionLoading(false);
|
|
68
|
+
}
|
|
69
|
+
} catch (e: unknown) {
|
|
70
|
+
setError((e as Error).message || 'Download failed');
|
|
71
|
+
setActionLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const isDownloading = status.downloading || actionLoading;
|
|
76
|
+
const showBanner = status.needsUpdate || isDownloading || justCompleted || error;
|
|
77
|
+
|
|
78
|
+
if (!showBanner && status.exists && !isDownloading) return null;
|
|
79
|
+
|
|
80
|
+
// ─── Downloading ───
|
|
81
|
+
if (isDownloading) {
|
|
82
|
+
return (
|
|
83
|
+
<Banner downloading>
|
|
84
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
|
|
85
|
+
<span style={{
|
|
86
|
+
display: 'inline-block', width: 8, height: 8,
|
|
87
|
+
borderRadius: '50%',
|
|
88
|
+
background: 'var(--violet)',
|
|
89
|
+
animation: 'pulse 1.5s ease-in-out infinite',
|
|
90
|
+
flexShrink: 0,
|
|
91
|
+
}} />
|
|
92
|
+
<span style={{ color: 'var(--violet)', fontWeight: 500 }}>Downloading agent base image…</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div style={{
|
|
95
|
+
flex: 1, maxWidth: 120, height: 3,
|
|
96
|
+
background: 'var(--border)', borderRadius: 2, overflow: 'hidden',
|
|
97
|
+
}}>
|
|
98
|
+
<div style={{
|
|
99
|
+
height: 3, background: 'var(--violet)',
|
|
100
|
+
animation: 'progress-indeterminate 1.5s ease-in-out infinite',
|
|
101
|
+
width: '40%', borderRadius: 2,
|
|
102
|
+
}} />
|
|
103
|
+
</div>
|
|
104
|
+
<Button variant="secondary" size="sm" disabled>Downloading…</Button>
|
|
105
|
+
</Banner>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Just completed ───
|
|
110
|
+
if (justCompleted && !isDownloading) {
|
|
111
|
+
return (
|
|
112
|
+
<Banner success>
|
|
113
|
+
<div style={{ flex: 1 }}>
|
|
114
|
+
<span style={{ color: '#22c55e', fontWeight: 500 }}>✓ Agent base image ready</span>
|
|
115
|
+
</div>
|
|
116
|
+
</Banner>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Needs download ───
|
|
121
|
+
return (
|
|
122
|
+
<Banner warning>
|
|
123
|
+
<div style={{ flex: 1 }}>
|
|
124
|
+
<strong style={{ color: '#fbbf24' }}>
|
|
125
|
+
{status.exists ? '⚠ Agent base image is outdated.' : '⚠ Agent base image not found.'}
|
|
126
|
+
</strong>
|
|
127
|
+
{' '}{status.exists
|
|
128
|
+
? 'A newer version is available on the registry.'
|
|
129
|
+
: 'Download it to create agent containers.'}
|
|
130
|
+
{error && <div style={{ color: '#ef4444', fontSize: 11, marginTop: 4 }}>{error}</div>}
|
|
131
|
+
</div>
|
|
132
|
+
<Button variant="primary" size="sm" onClick={handleDownload} loading={actionLoading}>
|
|
133
|
+
{actionLoading ? 'Starting…' : status.exists ? 'Download Update' : 'Download Image'}
|
|
134
|
+
</Button>
|
|
135
|
+
</Banner>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Banner({
|
|
140
|
+
children, warning = false, downloading = false, success = false,
|
|
141
|
+
}: {
|
|
142
|
+
children: React.ReactNode; warning?: boolean; downloading?: boolean; success?: boolean;
|
|
143
|
+
}) {
|
|
144
|
+
const bg = success ? 'rgba(34,197,94,0.08)' :
|
|
145
|
+
downloading ? 'rgba(146,91,252,0.1)' :
|
|
146
|
+
warning ? 'rgba(251,191,36,0.08)' : 'rgba(146,91,252,0.08)';
|
|
147
|
+
const border = success ? 'rgba(34,197,94,0.25)' :
|
|
148
|
+
downloading ? 'rgba(146,91,252,0.25)' :
|
|
149
|
+
warning ? 'rgba(251,191,36,0.2)' : 'rgba(146,91,252,0.2)';
|
|
150
|
+
return (
|
|
151
|
+
<div style={{
|
|
152
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8,
|
|
153
|
+
padding: '12px 14px', background: bg, borderBottom: `1px solid ${border}`,
|
|
154
|
+
fontSize: 12, flexShrink: 0,
|
|
155
|
+
}}>
|
|
156
|
+
{children}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|