thepopebot 1.1.2 → 1.2.3
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 +1 -1
- package/api/index.js +72 -165
- package/bin/cli.js +36 -6
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +6 -2
- package/config/index.js +2 -11
- package/config/instrumentation.js +17 -5
- package/lib/actions.js +7 -6
- package/lib/ai/agent.js +36 -0
- package/lib/ai/index.js +274 -0
- package/lib/ai/model.js +67 -0
- package/lib/ai/tools.js +49 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +45 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +30 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +146 -0
- package/lib/chat/actions.js +239 -0
- package/lib/chat/api.js +103 -0
- package/lib/chat/components/app-sidebar.js +161 -0
- package/lib/chat/components/app-sidebar.jsx +214 -0
- package/lib/chat/components/chat-header.js +9 -0
- package/lib/chat/components/chat-header.jsx +14 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +232 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +70 -0
- package/lib/chat/components/chat-page.jsx +89 -0
- package/lib/chat/components/chat.js +78 -0
- package/lib/chat/components/chat.jsx +91 -0
- package/lib/chat/components/chats-page.js +170 -0
- package/lib/chat/components/chats-page.jsx +203 -0
- package/lib/chat/components/crons-page.js +144 -0
- package/lib/chat/components/crons-page.jsx +204 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +14 -0
- package/lib/chat/components/icons.js +518 -0
- package/lib/chat/components/icons.jsx +482 -0
- package/lib/chat/components/index.js +19 -0
- package/lib/chat/components/message.js +66 -0
- package/lib/chat/components/message.jsx +92 -0
- package/lib/chat/components/messages.js +63 -0
- package/lib/chat/components/messages.jsx +72 -0
- package/lib/chat/components/notifications-page.js +54 -0
- package/lib/chat/components/notifications-page.jsx +83 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/settings-layout.js +37 -0
- package/lib/chat/components/settings-layout.jsx +51 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +54 -0
- package/lib/chat/components/sidebar-history-item.jsx +50 -0
- package/lib/chat/components/sidebar-history.js +92 -0
- package/lib/chat/components/sidebar-history.jsx +132 -0
- package/lib/chat/components/sidebar-user-nav.js +59 -0
- package/lib/chat/components/sidebar-user-nav.jsx +69 -0
- package/lib/chat/components/swarm-page.js +250 -0
- package/lib/chat/components/swarm-page.jsx +356 -0
- package/lib/chat/components/triggers-page.js +121 -0
- package/lib/chat/components/triggers-page.jsx +177 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +7 -8
- package/lib/db/api-keys.js +160 -0
- package/lib/db/chats.js +129 -0
- package/lib/db/index.js +106 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +51 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +23 -17
- package/lib/tools/create-job.js +3 -3
- package/lib/tools/github.js +145 -1
- package/lib/tools/openai.js +1 -1
- package/lib/tools/telegram.js +4 -3
- package/lib/triggers.js +6 -7
- package/lib/utils/render-md.js +6 -6
- package/package.json +43 -6
- package/setup/lib/auth.mjs +22 -9
- package/setup/lib/prerequisites.mjs +10 -3
- package/setup/lib/telegram-verify.mjs +3 -16
- package/setup/setup-telegram.mjs +31 -62
- package/setup/setup.mjs +58 -98
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +18 -2
- package/templates/.github/workflows/auto-merge.yml +1 -1
- package/templates/.github/workflows/build-image.yml +6 -4
- package/templates/.github/workflows/notify-job-failed.yml +2 -2
- package/templates/.github/workflows/notify-pr-complete.yml +2 -2
- package/templates/.github/workflows/run-job.yml +24 -10
- package/templates/CLAUDE.md +5 -3
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/api/chat/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +81 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +7 -0
- package/templates/app/globals.css +66 -0
- package/templates/app/layout.js +9 -2
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +6 -30
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +7 -0
- package/templates/config/CRONS.json +2 -2
- package/templates/config/TRIGGERS.json +2 -2
- package/templates/docker/event_handler/Dockerfile +19 -0
- package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
- package/templates/docker/runner/Dockerfile +38 -0
- package/templates/docker/runner/entrypoint.sh +41 -0
- package/templates/docker-compose.yml +52 -0
- package/templates/instrumentation.js +6 -1
- package/templates/middleware.js +1 -0
- package/templates/postcss.config.mjs +5 -0
- package/lib/claude/conversation.js +0 -76
- package/lib/claude/index.js +0 -142
- package/lib/claude/tools.js +0 -54
- /package/templates/docker/{Dockerfile → job/Dockerfile} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { signOut } from 'next-auth/react';
|
|
4
|
+
import { useTheme } from 'next-themes';
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from './ui/dropdown-menu.js';
|
|
13
|
+
import { SidebarMenu, SidebarMenuItem, SidebarMenuButton } from './ui/sidebar.js';
|
|
14
|
+
import { SettingsIcon, SunIcon, MoonIcon, LogOutIcon } from './icons.js';
|
|
15
|
+
import { cn } from '../utils.js';
|
|
16
|
+
|
|
17
|
+
export function SidebarUserNav({ user, collapsed }) {
|
|
18
|
+
const { theme, setTheme } = useTheme();
|
|
19
|
+
const [mounted, setMounted] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => setMounted(true), []);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<SidebarMenu>
|
|
25
|
+
<SidebarMenuItem>
|
|
26
|
+
<DropdownMenu>
|
|
27
|
+
<DropdownMenuTrigger asChild>
|
|
28
|
+
<SidebarMenuButton className={cn(collapsed ? 'justify-center' : 'justify-between')}>
|
|
29
|
+
<div className={cn('flex items-center overflow-hidden', collapsed ? '' : 'gap-2')}>
|
|
30
|
+
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
|
|
31
|
+
{(user?.email?.[0] || 'U').toUpperCase()}
|
|
32
|
+
</div>
|
|
33
|
+
{!collapsed && (
|
|
34
|
+
<span className="truncate text-sm">{user?.email || 'User'}</span>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
{!collapsed && (
|
|
38
|
+
<svg className="size-4 text-muted-foreground" width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
39
|
+
<path d="m7 15 5 5 5-5" />
|
|
40
|
+
<path d="m7 9 5-5 5 5" />
|
|
41
|
+
</svg>
|
|
42
|
+
)}
|
|
43
|
+
</SidebarMenuButton>
|
|
44
|
+
</DropdownMenuTrigger>
|
|
45
|
+
<DropdownMenuContent align="start" side="top" className="w-56">
|
|
46
|
+
<DropdownMenuItem onClick={() => { window.location.href = '/settings'; }}>
|
|
47
|
+
<SettingsIcon size={14} />
|
|
48
|
+
<span className="ml-2">Settings</span>
|
|
49
|
+
</DropdownMenuItem>
|
|
50
|
+
{mounted && (
|
|
51
|
+
<DropdownMenuItem onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
|
52
|
+
{theme === 'dark' ? <SunIcon size={14} /> : <MoonIcon size={14} />}
|
|
53
|
+
<span className="ml-2">{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
|
|
54
|
+
</DropdownMenuItem>
|
|
55
|
+
)}
|
|
56
|
+
<DropdownMenuSeparator />
|
|
57
|
+
<DropdownMenuItem
|
|
58
|
+
onClick={() => signOut({ callbackUrl: '/' })}
|
|
59
|
+
className="text-destructive"
|
|
60
|
+
>
|
|
61
|
+
<LogOutIcon size={14} />
|
|
62
|
+
<span className="ml-2">Sign Out</span>
|
|
63
|
+
</DropdownMenuItem>
|
|
64
|
+
</DropdownMenuContent>
|
|
65
|
+
</DropdownMenu>
|
|
66
|
+
</SidebarMenuItem>
|
|
67
|
+
</SidebarMenu>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { PageLayout } from "./page-layout.js";
|
|
5
|
+
import { StopIcon, SpinnerIcon, RefreshIcon } from "./icons.js";
|
|
6
|
+
import { getSwarmStatus, cancelSwarmJob, rerunSwarmJob } from "../actions.js";
|
|
7
|
+
function formatDuration(seconds) {
|
|
8
|
+
if (seconds < 60) return `${seconds}s`;
|
|
9
|
+
const minutes = Math.floor(seconds / 60);
|
|
10
|
+
const secs = seconds % 60;
|
|
11
|
+
if (minutes < 60) return `${minutes}m ${secs}s`;
|
|
12
|
+
const hours = Math.floor(minutes / 60);
|
|
13
|
+
const mins = minutes % 60;
|
|
14
|
+
return `${hours}h ${mins}m`;
|
|
15
|
+
}
|
|
16
|
+
function timeAgo(timestamp) {
|
|
17
|
+
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1e3);
|
|
18
|
+
if (seconds < 60) return "just now";
|
|
19
|
+
const minutes = Math.floor(seconds / 60);
|
|
20
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
21
|
+
const hours = Math.floor(minutes / 60);
|
|
22
|
+
if (hours < 24) return `${hours}h ago`;
|
|
23
|
+
const days = Math.floor(hours / 24);
|
|
24
|
+
if (days < 30) return `${days}d ago`;
|
|
25
|
+
const months = Math.floor(days / 30);
|
|
26
|
+
return `${months}mo ago`;
|
|
27
|
+
}
|
|
28
|
+
function LoadingSkeleton() {
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
30
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-3", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-md bg-border/50" }, i)) }),
|
|
31
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-32 animate-pulse rounded-md bg-border/50" }),
|
|
32
|
+
[...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-24 animate-pulse rounded-md bg-border/50" }, i))
|
|
33
|
+
] });
|
|
34
|
+
}
|
|
35
|
+
function SwarmSummaryCards({ counts }) {
|
|
36
|
+
const cards = [
|
|
37
|
+
{ label: "Running", value: counts.running, color: "border-l-green-500", text: "text-green-500" },
|
|
38
|
+
{ label: "Queued", value: counts.queued, color: "border-l-yellow-500", text: "text-yellow-500" },
|
|
39
|
+
{ label: "Succeeded", value: counts.succeeded, color: "border-l-blue-500", text: "text-blue-500" },
|
|
40
|
+
{ label: "Failed", value: counts.failed, color: "border-l-red-500", text: "text-red-500" }
|
|
41
|
+
];
|
|
42
|
+
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-3", children: cards.map((card) => /* @__PURE__ */ jsxs(
|
|
43
|
+
"div",
|
|
44
|
+
{
|
|
45
|
+
className: `rounded-md border border-l-4 ${card.color} bg-card p-4`,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground uppercase tracking-wide", children: card.label }),
|
|
48
|
+
/* @__PURE__ */ jsx("p", { className: `text-2xl font-bold mt-1 ${card.text}`, children: card.value })
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
card.label
|
|
52
|
+
)) });
|
|
53
|
+
}
|
|
54
|
+
function SwarmActiveJobs({ jobs, onCancel }) {
|
|
55
|
+
if (!jobs || jobs.length === 0) {
|
|
56
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No active jobs." });
|
|
57
|
+
}
|
|
58
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: jobs.map((job) => {
|
|
59
|
+
const isRunning = job.status === "in_progress";
|
|
60
|
+
const progress = job.steps_total > 0 ? Math.round(job.steps_completed / job.steps_total * 100) : 0;
|
|
61
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card p-4", children: [
|
|
62
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
63
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
64
|
+
/* @__PURE__ */ jsx(
|
|
65
|
+
"span",
|
|
66
|
+
{
|
|
67
|
+
className: `inline-block h-2.5 w-2.5 rounded-full ${isRunning ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-sm font-medium", children: job.job_id.slice(0, 8) }),
|
|
71
|
+
job.workflow_name && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: job.workflow_name })
|
|
72
|
+
] }),
|
|
73
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
74
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatDuration(job.duration_seconds) }),
|
|
75
|
+
job.html_url && /* @__PURE__ */ jsx(
|
|
76
|
+
"a",
|
|
77
|
+
{
|
|
78
|
+
href: job.html_url,
|
|
79
|
+
target: "_blank",
|
|
80
|
+
rel: "noopener noreferrer",
|
|
81
|
+
className: "text-xs text-blue-500 hover:underline",
|
|
82
|
+
children: "View"
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
/* @__PURE__ */ jsx(
|
|
86
|
+
"button",
|
|
87
|
+
{
|
|
88
|
+
onClick: () => onCancel(job.run_id),
|
|
89
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
|
|
90
|
+
title: "Cancel job",
|
|
91
|
+
children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
] })
|
|
95
|
+
] }),
|
|
96
|
+
job.current_step && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-2 truncate", children: job.current_step }),
|
|
97
|
+
job.steps_total > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
98
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 h-1.5 rounded-full bg-border overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
99
|
+
"div",
|
|
100
|
+
{
|
|
101
|
+
className: "h-full rounded-full bg-green-500 transition-all duration-500",
|
|
102
|
+
style: { width: `${progress}%` }
|
|
103
|
+
}
|
|
104
|
+
) }),
|
|
105
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
|
|
106
|
+
job.steps_completed,
|
|
107
|
+
"/",
|
|
108
|
+
job.steps_total
|
|
109
|
+
] })
|
|
110
|
+
] })
|
|
111
|
+
] }, job.run_id);
|
|
112
|
+
}) });
|
|
113
|
+
}
|
|
114
|
+
function SwarmJobHistory({ jobs, onRerun }) {
|
|
115
|
+
if (!jobs || jobs.length === 0) {
|
|
116
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No completed jobs." });
|
|
117
|
+
}
|
|
118
|
+
const badgeStyles = {
|
|
119
|
+
success: "bg-green-500/10 text-green-500",
|
|
120
|
+
failure: "bg-red-500/10 text-red-500",
|
|
121
|
+
cancelled: "bg-yellow-500/10 text-yellow-500"
|
|
122
|
+
};
|
|
123
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: jobs.map((job) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-3 px-1", children: [
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
"span",
|
|
126
|
+
{
|
|
127
|
+
className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${badgeStyles[job.conclusion] || "bg-muted text-muted-foreground"}`,
|
|
128
|
+
children: job.conclusion || "unknown"
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-sm", children: job.job_id.slice(0, 8) }),
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(job.updated_at || job.started_at) }),
|
|
133
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
134
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
135
|
+
job.html_url && /* @__PURE__ */ jsx(
|
|
136
|
+
"a",
|
|
137
|
+
{
|
|
138
|
+
href: job.html_url,
|
|
139
|
+
target: "_blank",
|
|
140
|
+
rel: "noopener noreferrer",
|
|
141
|
+
className: "text-xs text-blue-500 hover:underline",
|
|
142
|
+
children: "View"
|
|
143
|
+
}
|
|
144
|
+
),
|
|
145
|
+
/* @__PURE__ */ jsx(
|
|
146
|
+
"button",
|
|
147
|
+
{
|
|
148
|
+
onClick: () => onRerun(job.run_id, false),
|
|
149
|
+
className: "text-xs rounded-md px-2 py-1 border hover:bg-accent",
|
|
150
|
+
children: "Rerun"
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
job.conclusion === "failure" && /* @__PURE__ */ jsx(
|
|
154
|
+
"button",
|
|
155
|
+
{
|
|
156
|
+
onClick: () => onRerun(job.run_id, true),
|
|
157
|
+
className: "text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10",
|
|
158
|
+
children: "Rerun failed"
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
] })
|
|
162
|
+
] }, job.run_id)) });
|
|
163
|
+
}
|
|
164
|
+
function SwarmPage({ session }) {
|
|
165
|
+
const [swarmData, setSwarmData] = useState(null);
|
|
166
|
+
const [loading, setLoading] = useState(true);
|
|
167
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
168
|
+
const fetchStatus = useCallback(async () => {
|
|
169
|
+
try {
|
|
170
|
+
const data = await getSwarmStatus();
|
|
171
|
+
setSwarmData(data);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error("Failed to fetch swarm status:", err);
|
|
174
|
+
} finally {
|
|
175
|
+
setLoading(false);
|
|
176
|
+
}
|
|
177
|
+
}, []);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
fetchStatus();
|
|
180
|
+
const interval = setInterval(fetchStatus, 1e4);
|
|
181
|
+
return () => clearInterval(interval);
|
|
182
|
+
}, [fetchStatus]);
|
|
183
|
+
const handleRefresh = async () => {
|
|
184
|
+
setRefreshing(true);
|
|
185
|
+
await fetchStatus();
|
|
186
|
+
setRefreshing(false);
|
|
187
|
+
};
|
|
188
|
+
const handleCancel = async (runId) => {
|
|
189
|
+
try {
|
|
190
|
+
await cancelSwarmJob(runId);
|
|
191
|
+
await fetchStatus();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error("Failed to cancel job:", err);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const handleRerun = async (runId, failedOnly) => {
|
|
197
|
+
try {
|
|
198
|
+
await rerunSwarmJob(runId, failedOnly);
|
|
199
|
+
await fetchStatus();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error("Failed to rerun job:", err);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
|
|
205
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-6", children: [
|
|
206
|
+
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Swarm" }),
|
|
207
|
+
!loading && /* @__PURE__ */ jsx(
|
|
208
|
+
"button",
|
|
209
|
+
{
|
|
210
|
+
onClick: handleRefresh,
|
|
211
|
+
disabled: refreshing,
|
|
212
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:pointer-events-none",
|
|
213
|
+
children: refreshing ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
214
|
+
/* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }),
|
|
215
|
+
"Refreshing..."
|
|
216
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
217
|
+
/* @__PURE__ */ jsx(RefreshIcon, { size: 14 }),
|
|
218
|
+
"Refresh"
|
|
219
|
+
] })
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
] }),
|
|
223
|
+
loading ? /* @__PURE__ */ jsx(LoadingSkeleton, {}) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6", children: [
|
|
224
|
+
swarmData?.counts && /* @__PURE__ */ jsx(SwarmSummaryCards, { counts: swarmData.counts }),
|
|
225
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
226
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Active Jobs" }),
|
|
227
|
+
/* @__PURE__ */ jsx(
|
|
228
|
+
SwarmActiveJobs,
|
|
229
|
+
{
|
|
230
|
+
jobs: swarmData?.active,
|
|
231
|
+
onCancel: handleCancel
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
] }),
|
|
235
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
236
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Recent History" }),
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
SwarmJobHistory,
|
|
239
|
+
{
|
|
240
|
+
jobs: swarmData?.completed,
|
|
241
|
+
onRerun: handleRerun
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
] })
|
|
245
|
+
] })
|
|
246
|
+
] });
|
|
247
|
+
}
|
|
248
|
+
export {
|
|
249
|
+
SwarmPage
|
|
250
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { PageLayout } from './page-layout.js';
|
|
5
|
+
import { StopIcon, SpinnerIcon, RefreshIcon } from './icons.js';
|
|
6
|
+
import { getSwarmStatus, cancelSwarmJob, rerunSwarmJob } from '../actions.js';
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Utilities
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function formatDuration(seconds) {
|
|
13
|
+
if (seconds < 60) return `${seconds}s`;
|
|
14
|
+
const minutes = Math.floor(seconds / 60);
|
|
15
|
+
const secs = seconds % 60;
|
|
16
|
+
if (minutes < 60) return `${minutes}m ${secs}s`;
|
|
17
|
+
const hours = Math.floor(minutes / 60);
|
|
18
|
+
const mins = minutes % 60;
|
|
19
|
+
return `${hours}h ${mins}m`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timeAgo(timestamp) {
|
|
23
|
+
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
24
|
+
if (seconds < 60) return 'just now';
|
|
25
|
+
const minutes = Math.floor(seconds / 60);
|
|
26
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
27
|
+
const hours = Math.floor(minutes / 60);
|
|
28
|
+
if (hours < 24) return `${hours}h ago`;
|
|
29
|
+
const days = Math.floor(hours / 24);
|
|
30
|
+
if (days < 30) return `${days}d ago`;
|
|
31
|
+
const months = Math.floor(days / 30);
|
|
32
|
+
return `${months}mo ago`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function LoadingSkeleton() {
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex flex-col gap-4">
|
|
38
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
39
|
+
{[...Array(4)].map((_, i) => (
|
|
40
|
+
<div key={i} className="h-20 animate-pulse rounded-md bg-border/50" />
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
<div className="h-8 w-32 animate-pulse rounded-md bg-border/50" />
|
|
44
|
+
{[...Array(3)].map((_, i) => (
|
|
45
|
+
<div key={i} className="h-24 animate-pulse rounded-md bg-border/50" />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// Summary Cards
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function SwarmSummaryCards({ counts }) {
|
|
56
|
+
const cards = [
|
|
57
|
+
{ label: 'Running', value: counts.running, color: 'border-l-green-500', text: 'text-green-500' },
|
|
58
|
+
{ label: 'Queued', value: counts.queued, color: 'border-l-yellow-500', text: 'text-yellow-500' },
|
|
59
|
+
{ label: 'Succeeded', value: counts.succeeded, color: 'border-l-blue-500', text: 'text-blue-500' },
|
|
60
|
+
{ label: 'Failed', value: counts.failed, color: 'border-l-red-500', text: 'text-red-500' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
65
|
+
{cards.map((card) => (
|
|
66
|
+
<div
|
|
67
|
+
key={card.label}
|
|
68
|
+
className={`rounded-md border border-l-4 ${card.color} bg-card p-4`}
|
|
69
|
+
>
|
|
70
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wide">{card.label}</p>
|
|
71
|
+
<p className={`text-2xl font-bold mt-1 ${card.text}`}>{card.value}</p>
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Active Jobs
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function SwarmActiveJobs({ jobs, onCancel }) {
|
|
83
|
+
if (!jobs || jobs.length === 0) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
86
|
+
No active jobs.
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-col gap-3">
|
|
93
|
+
{jobs.map((job) => {
|
|
94
|
+
const isRunning = job.status === 'in_progress';
|
|
95
|
+
const progress = job.steps_total > 0
|
|
96
|
+
? Math.round((job.steps_completed / job.steps_total) * 100)
|
|
97
|
+
: 0;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div key={job.run_id} className="rounded-md border bg-card p-4">
|
|
101
|
+
<div className="flex items-center justify-between mb-2">
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
{/* Status dot */}
|
|
104
|
+
<span
|
|
105
|
+
className={`inline-block h-2.5 w-2.5 rounded-full ${
|
|
106
|
+
isRunning ? 'bg-green-500 animate-pulse' : 'bg-yellow-500'
|
|
107
|
+
}`}
|
|
108
|
+
/>
|
|
109
|
+
<span className="font-mono text-sm font-medium">
|
|
110
|
+
{job.job_id.slice(0, 8)}
|
|
111
|
+
</span>
|
|
112
|
+
{job.workflow_name && (
|
|
113
|
+
<span className="text-xs text-muted-foreground">
|
|
114
|
+
{job.workflow_name}
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<span className="text-xs text-muted-foreground">
|
|
120
|
+
{formatDuration(job.duration_seconds)}
|
|
121
|
+
</span>
|
|
122
|
+
{job.html_url && (
|
|
123
|
+
<a
|
|
124
|
+
href={job.html_url}
|
|
125
|
+
target="_blank"
|
|
126
|
+
rel="noopener noreferrer"
|
|
127
|
+
className="text-xs text-blue-500 hover:underline"
|
|
128
|
+
>
|
|
129
|
+
View
|
|
130
|
+
</a>
|
|
131
|
+
)}
|
|
132
|
+
<button
|
|
133
|
+
onClick={() => onCancel(job.run_id)}
|
|
134
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
|
135
|
+
title="Cancel job"
|
|
136
|
+
>
|
|
137
|
+
<StopIcon size={14} />
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Current step */}
|
|
143
|
+
{job.current_step && (
|
|
144
|
+
<p className="text-xs text-muted-foreground mb-2 truncate">
|
|
145
|
+
{job.current_step}
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{/* Progress bar */}
|
|
150
|
+
{job.steps_total > 0 && (
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<div className="flex-1 h-1.5 rounded-full bg-border overflow-hidden">
|
|
153
|
+
<div
|
|
154
|
+
className="h-full rounded-full bg-green-500 transition-all duration-500"
|
|
155
|
+
style={{ width: `${progress}%` }}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
<span className="text-xs text-muted-foreground shrink-0">
|
|
159
|
+
{job.steps_completed}/{job.steps_total}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// Job History
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function SwarmJobHistory({ jobs, onRerun }) {
|
|
175
|
+
if (!jobs || jobs.length === 0) {
|
|
176
|
+
return (
|
|
177
|
+
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
178
|
+
No completed jobs.
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const badgeStyles = {
|
|
184
|
+
success: 'bg-green-500/10 text-green-500',
|
|
185
|
+
failure: 'bg-red-500/10 text-red-500',
|
|
186
|
+
cancelled: 'bg-yellow-500/10 text-yellow-500',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex flex-col divide-y divide-border">
|
|
191
|
+
{jobs.map((job) => (
|
|
192
|
+
<div key={job.run_id} className="flex items-center gap-3 py-3 px-1">
|
|
193
|
+
{/* Conclusion badge */}
|
|
194
|
+
<span
|
|
195
|
+
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${
|
|
196
|
+
badgeStyles[job.conclusion] || 'bg-muted text-muted-foreground'
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{job.conclusion || 'unknown'}
|
|
200
|
+
</span>
|
|
201
|
+
|
|
202
|
+
{/* Job ID */}
|
|
203
|
+
<span className="font-mono text-sm">{job.job_id.slice(0, 8)}</span>
|
|
204
|
+
|
|
205
|
+
{/* Time ago */}
|
|
206
|
+
<span className="text-xs text-muted-foreground">
|
|
207
|
+
{timeAgo(job.updated_at || job.started_at)}
|
|
208
|
+
</span>
|
|
209
|
+
|
|
210
|
+
{/* Spacer */}
|
|
211
|
+
<div className="flex-1" />
|
|
212
|
+
|
|
213
|
+
{/* Actions */}
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
{job.html_url && (
|
|
216
|
+
<a
|
|
217
|
+
href={job.html_url}
|
|
218
|
+
target="_blank"
|
|
219
|
+
rel="noopener noreferrer"
|
|
220
|
+
className="text-xs text-blue-500 hover:underline"
|
|
221
|
+
>
|
|
222
|
+
View
|
|
223
|
+
</a>
|
|
224
|
+
)}
|
|
225
|
+
<button
|
|
226
|
+
onClick={() => onRerun(job.run_id, false)}
|
|
227
|
+
className="text-xs rounded-md px-2 py-1 border hover:bg-accent"
|
|
228
|
+
>
|
|
229
|
+
Rerun
|
|
230
|
+
</button>
|
|
231
|
+
{job.conclusion === 'failure' && (
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => onRerun(job.run_id, true)}
|
|
234
|
+
className="text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10"
|
|
235
|
+
>
|
|
236
|
+
Rerun failed
|
|
237
|
+
</button>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
// Main Page
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
export function SwarmPage({ session }) {
|
|
251
|
+
const [swarmData, setSwarmData] = useState(null);
|
|
252
|
+
const [loading, setLoading] = useState(true);
|
|
253
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
254
|
+
|
|
255
|
+
const fetchStatus = useCallback(async () => {
|
|
256
|
+
try {
|
|
257
|
+
const data = await getSwarmStatus();
|
|
258
|
+
setSwarmData(data);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error('Failed to fetch swarm status:', err);
|
|
261
|
+
} finally {
|
|
262
|
+
setLoading(false);
|
|
263
|
+
}
|
|
264
|
+
}, []);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
fetchStatus();
|
|
268
|
+
const interval = setInterval(fetchStatus, 10000);
|
|
269
|
+
return () => clearInterval(interval);
|
|
270
|
+
}, [fetchStatus]);
|
|
271
|
+
|
|
272
|
+
const handleRefresh = async () => {
|
|
273
|
+
setRefreshing(true);
|
|
274
|
+
await fetchStatus();
|
|
275
|
+
setRefreshing(false);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleCancel = async (runId) => {
|
|
279
|
+
try {
|
|
280
|
+
await cancelSwarmJob(runId);
|
|
281
|
+
await fetchStatus();
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error('Failed to cancel job:', err);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const handleRerun = async (runId, failedOnly) => {
|
|
288
|
+
try {
|
|
289
|
+
await rerunSwarmJob(runId, failedOnly);
|
|
290
|
+
await fetchStatus();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error('Failed to rerun job:', err);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<PageLayout session={session}>
|
|
298
|
+
{/* Header */}
|
|
299
|
+
<div className="flex items-center justify-between mb-6">
|
|
300
|
+
<h1 className="text-2xl font-semibold">Swarm</h1>
|
|
301
|
+
{!loading && (
|
|
302
|
+
<button
|
|
303
|
+
onClick={handleRefresh}
|
|
304
|
+
disabled={refreshing}
|
|
305
|
+
className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:pointer-events-none"
|
|
306
|
+
>
|
|
307
|
+
{refreshing ? (
|
|
308
|
+
<>
|
|
309
|
+
<SpinnerIcon size={14} />
|
|
310
|
+
Refreshing...
|
|
311
|
+
</>
|
|
312
|
+
) : (
|
|
313
|
+
<>
|
|
314
|
+
<RefreshIcon size={14} />
|
|
315
|
+
Refresh
|
|
316
|
+
</>
|
|
317
|
+
)}
|
|
318
|
+
</button>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{loading ? (
|
|
323
|
+
<LoadingSkeleton />
|
|
324
|
+
) : (
|
|
325
|
+
<div className="flex flex-col gap-6">
|
|
326
|
+
{/* Summary Cards */}
|
|
327
|
+
{swarmData?.counts && (
|
|
328
|
+
<SwarmSummaryCards counts={swarmData.counts} />
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Active Jobs */}
|
|
332
|
+
<div>
|
|
333
|
+
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
|
|
334
|
+
Active Jobs
|
|
335
|
+
</h2>
|
|
336
|
+
<SwarmActiveJobs
|
|
337
|
+
jobs={swarmData?.active}
|
|
338
|
+
onCancel={handleCancel}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{/* Job History */}
|
|
343
|
+
<div>
|
|
344
|
+
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
|
|
345
|
+
Recent History
|
|
346
|
+
</h2>
|
|
347
|
+
<SwarmJobHistory
|
|
348
|
+
jobs={swarmData?.completed}
|
|
349
|
+
onRerun={handleRerun}
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
</PageLayout>
|
|
355
|
+
);
|
|
356
|
+
}
|