thepopebot 1.2.39 → 1.2.41
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/config/instrumentation.js +11 -0
- package/lib/chat/actions.js +16 -3
- package/lib/chat/components/app-sidebar.js +34 -2
- package/lib/chat/components/app-sidebar.jsx +40 -2
- package/lib/chat/components/icons.js +22 -0
- package/lib/chat/components/icons.jsx +20 -0
- package/lib/chat/components/index.js +1 -0
- package/lib/chat/components/upgrade-page.js +109 -0
- package/lib/chat/components/upgrade-page.jsx +141 -0
- package/lib/cron.js +88 -1
- package/lib/db/update-check.js +50 -0
- package/lib/tools/github.js +29 -0
- package/package.json +1 -1
- package/templates/.github/workflows/upgrade-event-handler.yml +39 -0
- package/templates/app/upgrade/page.js +7 -0
|
@@ -42,5 +42,16 @@ export async function register() {
|
|
|
42
42
|
const { loadCrons } = await import('../lib/cron.js');
|
|
43
43
|
loadCrons();
|
|
44
44
|
|
|
45
|
+
// Start built-in crons (version check)
|
|
46
|
+
const { startBuiltinCrons, setUpdateAvailable } = await import('../lib/cron.js');
|
|
47
|
+
startBuiltinCrons();
|
|
48
|
+
|
|
49
|
+
// Warm in-memory flag from DB (covers the window before the async cron fetch completes)
|
|
50
|
+
try {
|
|
51
|
+
const { getAvailableVersion } = await import('../lib/db/update-check.js');
|
|
52
|
+
const stored = getAvailableVersion();
|
|
53
|
+
if (stored) setUpdateAvailable(stored);
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
45
56
|
console.log('thepopebot initialized');
|
|
46
57
|
}
|
package/lib/chat/actions.js
CHANGED
|
@@ -159,14 +159,27 @@ export async function markNotificationsRead() {
|
|
|
159
159
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* Get the installed package version (auth-gated, never in client bundle).
|
|
163
|
-
* @returns {Promise<string>}
|
|
162
|
+
* Get the installed package version and update status (auth-gated, never in client bundle).
|
|
163
|
+
* @returns {Promise<{ version: string, updateAvailable: string|null }>}
|
|
164
164
|
*/
|
|
165
165
|
export async function getAppVersion() {
|
|
166
166
|
await requireAuth();
|
|
167
167
|
const { createRequire } = await import('module');
|
|
168
168
|
const require = createRequire(import.meta.url);
|
|
169
|
-
|
|
169
|
+
const version = require('../../../package.json').version;
|
|
170
|
+
const { getUpdateAvailable } = await import('../cron.js');
|
|
171
|
+
return { version, updateAvailable: getUpdateAvailable() };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Trigger the upgrade-event-handler workflow via GitHub Actions.
|
|
176
|
+
* @returns {Promise<{ success: boolean }>}
|
|
177
|
+
*/
|
|
178
|
+
export async function triggerUpgrade() {
|
|
179
|
+
await requireAuth();
|
|
180
|
+
const { triggerWorkflowDispatch } = await import('../tools/github.js');
|
|
181
|
+
await triggerWorkflowDispatch('upgrade-event-handler.yml');
|
|
182
|
+
return { success: true };
|
|
170
183
|
}
|
|
171
184
|
|
|
172
185
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
-
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon } from "./icons.js";
|
|
4
|
+
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon } from "./icons.js";
|
|
5
5
|
import { getUnreadNotificationCount, getAppVersion } from "../actions.js";
|
|
6
6
|
import { SidebarHistory } from "./sidebar-history.js";
|
|
7
7
|
import { SidebarUserNav } from "./sidebar-user-nav.js";
|
|
@@ -25,10 +25,14 @@ function AppSidebar({ user }) {
|
|
|
25
25
|
const collapsed = state === "collapsed";
|
|
26
26
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
27
27
|
const [version, setVersion] = useState("");
|
|
28
|
+
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
28
29
|
useEffect(() => {
|
|
29
30
|
getUnreadNotificationCount().then((count) => setUnreadCount(count)).catch(() => {
|
|
30
31
|
});
|
|
31
|
-
getAppVersion().then(
|
|
32
|
+
getAppVersion().then(({ version: version2, updateAvailable: updateAvailable2 }) => {
|
|
33
|
+
setVersion(version2);
|
|
34
|
+
setUpdateAvailable(updateAvailable2);
|
|
35
|
+
}).catch(() => {
|
|
32
36
|
});
|
|
33
37
|
}, []);
|
|
34
38
|
return /* @__PURE__ */ jsxs(Sidebar, { children: [
|
|
@@ -122,6 +126,34 @@ function AppSidebar({ user }) {
|
|
|
122
126
|
}
|
|
123
127
|
) }),
|
|
124
128
|
collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Notifications" })
|
|
129
|
+
] }) }),
|
|
130
|
+
updateAvailable && /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
131
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
132
|
+
SidebarMenuButton,
|
|
133
|
+
{
|
|
134
|
+
className: collapsed ? "justify-center" : "",
|
|
135
|
+
onClick: () => {
|
|
136
|
+
window.location.href = "/upgrade";
|
|
137
|
+
},
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ jsxs("span", { className: "relative", children: [
|
|
140
|
+
/* @__PURE__ */ jsx(ArrowUpCircleIcon, { size: 16 }),
|
|
141
|
+
collapsed && /* @__PURE__ */ jsx("span", { className: "absolute -top-1 -right-1 inline-block h-2 w-2 rounded-full bg-blue-500" })
|
|
142
|
+
] }),
|
|
143
|
+
!collapsed && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
144
|
+
"Upgrade",
|
|
145
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center justify-center rounded-full bg-blue-500 px-1.5 py-0.5 text-[10px] font-medium leading-none text-white", children: [
|
|
146
|
+
"v",
|
|
147
|
+
updateAvailable
|
|
148
|
+
] })
|
|
149
|
+
] })
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
) }),
|
|
153
|
+
collapsed && /* @__PURE__ */ jsxs(TooltipContent, { side: "right", children: [
|
|
154
|
+
"Upgrade to v",
|
|
155
|
+
updateAvailable
|
|
156
|
+
] })
|
|
125
157
|
] }) })
|
|
126
158
|
] })
|
|
127
159
|
] }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon } from './icons.js';
|
|
4
|
+
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon } from './icons.js';
|
|
5
5
|
import { getUnreadNotificationCount, getAppVersion } from '../actions.js';
|
|
6
6
|
import { SidebarHistory } from './sidebar-history.js';
|
|
7
7
|
import { SidebarUserNav } from './sidebar-user-nav.js';
|
|
@@ -26,13 +26,17 @@ export function AppSidebar({ user }) {
|
|
|
26
26
|
const collapsed = state === 'collapsed';
|
|
27
27
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
28
28
|
const [version, setVersion] = useState('');
|
|
29
|
+
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
29
30
|
|
|
30
31
|
useEffect(() => {
|
|
31
32
|
getUnreadNotificationCount()
|
|
32
33
|
.then((count) => setUnreadCount(count))
|
|
33
34
|
.catch(() => {});
|
|
34
35
|
getAppVersion()
|
|
35
|
-
.then(
|
|
36
|
+
.then(({ version, updateAvailable }) => {
|
|
37
|
+
setVersion(version);
|
|
38
|
+
setUpdateAvailable(updateAvailable);
|
|
39
|
+
})
|
|
36
40
|
.catch(() => {});
|
|
37
41
|
}, []);
|
|
38
42
|
|
|
@@ -155,6 +159,40 @@ export function AppSidebar({ user }) {
|
|
|
155
159
|
</Tooltip>
|
|
156
160
|
</SidebarMenuItem>
|
|
157
161
|
|
|
162
|
+
{/* Upgrade (only when update is available) */}
|
|
163
|
+
{updateAvailable && (
|
|
164
|
+
<SidebarMenuItem>
|
|
165
|
+
<Tooltip>
|
|
166
|
+
<TooltipTrigger asChild>
|
|
167
|
+
<SidebarMenuButton
|
|
168
|
+
className={collapsed ? 'justify-center' : ''}
|
|
169
|
+
onClick={() => {
|
|
170
|
+
window.location.href = '/upgrade';
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<span className="relative">
|
|
174
|
+
<ArrowUpCircleIcon size={16} />
|
|
175
|
+
{collapsed && (
|
|
176
|
+
<span className="absolute -top-1 -right-1 inline-block h-2 w-2 rounded-full bg-blue-500" />
|
|
177
|
+
)}
|
|
178
|
+
</span>
|
|
179
|
+
{!collapsed && (
|
|
180
|
+
<span className="flex items-center gap-2">
|
|
181
|
+
Upgrade
|
|
182
|
+
<span className="inline-flex items-center justify-center rounded-full bg-blue-500 px-1.5 py-0.5 text-[10px] font-medium leading-none text-white">
|
|
183
|
+
v{updateAvailable}
|
|
184
|
+
</span>
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
</SidebarMenuButton>
|
|
188
|
+
</TooltipTrigger>
|
|
189
|
+
{collapsed && (
|
|
190
|
+
<TooltipContent side="right">Upgrade to v{updateAvailable}</TooltipContent>
|
|
191
|
+
)}
|
|
192
|
+
</Tooltip>
|
|
193
|
+
</SidebarMenuItem>
|
|
194
|
+
)}
|
|
195
|
+
|
|
158
196
|
</SidebarMenu>
|
|
159
197
|
</SidebarHeader>
|
|
160
198
|
|
|
@@ -561,6 +561,27 @@ function CirclePlusIcon({ size = 16 }) {
|
|
|
561
561
|
}
|
|
562
562
|
);
|
|
563
563
|
}
|
|
564
|
+
function ArrowUpCircleIcon({ size = 16 }) {
|
|
565
|
+
return /* @__PURE__ */ jsxs(
|
|
566
|
+
"svg",
|
|
567
|
+
{
|
|
568
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
569
|
+
viewBox: "0 0 24 24",
|
|
570
|
+
fill: "none",
|
|
571
|
+
stroke: "currentColor",
|
|
572
|
+
strokeWidth: 2,
|
|
573
|
+
strokeLinecap: "round",
|
|
574
|
+
strokeLinejoin: "round",
|
|
575
|
+
width: size,
|
|
576
|
+
height: size,
|
|
577
|
+
children: [
|
|
578
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
579
|
+
/* @__PURE__ */ jsx("path", { d: "m16 12-4-4-4 4" }),
|
|
580
|
+
/* @__PURE__ */ jsx("path", { d: "M12 16V8" })
|
|
581
|
+
]
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
}
|
|
564
585
|
function LogOutIcon({ size = 16 }) {
|
|
565
586
|
return /* @__PURE__ */ jsxs(
|
|
566
587
|
"svg",
|
|
@@ -583,6 +604,7 @@ function LogOutIcon({ size = 16 }) {
|
|
|
583
604
|
);
|
|
584
605
|
}
|
|
585
606
|
export {
|
|
607
|
+
ArrowUpCircleIcon,
|
|
586
608
|
BellIcon,
|
|
587
609
|
CheckIcon,
|
|
588
610
|
ChevronDownIcon,
|
|
@@ -555,6 +555,26 @@ export function CirclePlusIcon({ size = 16 }) {
|
|
|
555
555
|
);
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
export function ArrowUpCircleIcon({ size = 16 }) {
|
|
559
|
+
return (
|
|
560
|
+
<svg
|
|
561
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
562
|
+
viewBox="0 0 24 24"
|
|
563
|
+
fill="none"
|
|
564
|
+
stroke="currentColor"
|
|
565
|
+
strokeWidth={2}
|
|
566
|
+
strokeLinecap="round"
|
|
567
|
+
strokeLinejoin="round"
|
|
568
|
+
width={size}
|
|
569
|
+
height={size}
|
|
570
|
+
>
|
|
571
|
+
<circle cx="12" cy="12" r="10" />
|
|
572
|
+
<path d="m16 12-4-4-4 4" />
|
|
573
|
+
<path d="M12 16V8" />
|
|
574
|
+
</svg>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
558
578
|
export function LogOutIcon({ size = 16 }) {
|
|
559
579
|
return (
|
|
560
580
|
<svg
|
|
@@ -2,6 +2,7 @@ export { ChatPage } from './chat-page.js';
|
|
|
2
2
|
export { ChatsPage } from './chats-page.js';
|
|
3
3
|
export { NotificationsPage } from './notifications-page.js';
|
|
4
4
|
export { SwarmPage } from './swarm-page.js';
|
|
5
|
+
export { UpgradePage } from './upgrade-page.js';
|
|
5
6
|
export { CronsPage } from './crons-page.js';
|
|
6
7
|
export { TriggersPage } from './triggers-page.js';
|
|
7
8
|
export { PageLayout } from './page-layout.js';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { PageLayout } from "./page-layout.js";
|
|
5
|
+
import { ArrowUpCircleIcon, SpinnerIcon, CheckIcon } from "./icons.js";
|
|
6
|
+
import { getAppVersion, triggerUpgrade } from "../actions.js";
|
|
7
|
+
function UpgradePage({ session }) {
|
|
8
|
+
const [version, setVersion] = useState("");
|
|
9
|
+
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
12
|
+
const [result, setResult] = useState(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
getAppVersion().then(({ version: version2, updateAvailable: updateAvailable2 }) => {
|
|
15
|
+
setVersion(version2);
|
|
16
|
+
setUpdateAvailable(updateAvailable2);
|
|
17
|
+
}).catch(() => {
|
|
18
|
+
}).finally(() => setLoading(false));
|
|
19
|
+
}, []);
|
|
20
|
+
const handleUpgrade = async () => {
|
|
21
|
+
setUpgrading(true);
|
|
22
|
+
setResult(null);
|
|
23
|
+
try {
|
|
24
|
+
await triggerUpgrade();
|
|
25
|
+
setResult("success");
|
|
26
|
+
} catch {
|
|
27
|
+
setResult("error");
|
|
28
|
+
} finally {
|
|
29
|
+
setUpgrading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
|
|
33
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-6", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Upgrade" }) }),
|
|
34
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx("div", { className: "h-32 animate-pulse rounded-md bg-border/50" }) }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6 max-w-lg", children: [
|
|
35
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card p-6", children: [
|
|
36
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
|
|
37
|
+
/* @__PURE__ */ jsx(ArrowUpCircleIcon, { size: 24 }),
|
|
38
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
39
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Installed version" }),
|
|
40
|
+
/* @__PURE__ */ jsxs("p", { className: "text-lg font-mono font-semibold", children: [
|
|
41
|
+
"v",
|
|
42
|
+
version
|
|
43
|
+
] })
|
|
44
|
+
] })
|
|
45
|
+
] }),
|
|
46
|
+
updateAvailable ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-blue-500/30 bg-blue-500/5 p-4 mb-4", children: /* @__PURE__ */ jsxs("p", { className: "text-sm font-medium", children: [
|
|
47
|
+
"Version ",
|
|
48
|
+
/* @__PURE__ */ jsxs("span", { className: "font-mono text-blue-500", children: [
|
|
49
|
+
"v",
|
|
50
|
+
updateAvailable
|
|
51
|
+
] }),
|
|
52
|
+
" is available"
|
|
53
|
+
] }) }) : /* @__PURE__ */ jsx("div", { className: "rounded-md border border-green-500/30 bg-green-500/5 p-4 mb-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: "You are on the latest version" }) }),
|
|
54
|
+
updateAvailable && /* @__PURE__ */ jsx(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
onClick: handleUpgrade,
|
|
58
|
+
disabled: upgrading || result === "success",
|
|
59
|
+
className: "w-full inline-flex items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:pointer-events-none",
|
|
60
|
+
children: upgrading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
61
|
+
/* @__PURE__ */ jsx(SpinnerIcon, { size: 16 }),
|
|
62
|
+
"Triggering upgrade..."
|
|
63
|
+
] }) : result === "success" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
64
|
+
/* @__PURE__ */ jsx(CheckIcon, { size: 16 }),
|
|
65
|
+
"Upgrade triggered"
|
|
66
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
67
|
+
/* @__PURE__ */ jsx(ArrowUpCircleIcon, { size: 16 }),
|
|
68
|
+
"Upgrade to v",
|
|
69
|
+
updateAvailable
|
|
70
|
+
] })
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
result === "success" && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-3", children: "The upgrade workflow has been triggered. The server will update, rebuild, and reload automatically. This page will reflect the new version once the process completes." }),
|
|
74
|
+
result === "error" && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500 mt-3", children: "Failed to trigger the upgrade workflow. Check that your GitHub token has workflow permissions." })
|
|
75
|
+
] }),
|
|
76
|
+
updateAvailable && /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card p-6", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-medium mb-3", children: "What happens during an upgrade" }),
|
|
78
|
+
/* @__PURE__ */ jsxs("ul", { className: "text-sm text-muted-foreground space-y-2", children: [
|
|
79
|
+
/* @__PURE__ */ jsxs("li", { className: "flex gap-2", children: [
|
|
80
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: "1." }),
|
|
81
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
82
|
+
"The ",
|
|
83
|
+
/* @__PURE__ */ jsx("code", { className: "text-xs bg-muted px-1 py-0.5 rounded", children: "upgrade-event-handler" }),
|
|
84
|
+
" GitHub Actions workflow is triggered"
|
|
85
|
+
] })
|
|
86
|
+
] }),
|
|
87
|
+
/* @__PURE__ */ jsxs("li", { className: "flex gap-2", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: "2." }),
|
|
89
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
90
|
+
"The package is updated via ",
|
|
91
|
+
/* @__PURE__ */ jsx("code", { className: "text-xs bg-muted px-1 py-0.5 rounded", children: "npm update thepopebot" })
|
|
92
|
+
] })
|
|
93
|
+
] }),
|
|
94
|
+
/* @__PURE__ */ jsxs("li", { className: "flex gap-2", children: [
|
|
95
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: "3." }),
|
|
96
|
+
/* @__PURE__ */ jsx("span", { children: "A new build is created alongside the current one" })
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ jsxs("li", { className: "flex gap-2", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: "4." }),
|
|
100
|
+
/* @__PURE__ */ jsx("span", { children: "The builds are atomically swapped and the server reloads" })
|
|
101
|
+
] })
|
|
102
|
+
] })
|
|
103
|
+
] })
|
|
104
|
+
] })
|
|
105
|
+
] });
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
UpgradePage
|
|
109
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { PageLayout } from './page-layout.js';
|
|
5
|
+
import { ArrowUpCircleIcon, SpinnerIcon, CheckIcon } from './icons.js';
|
|
6
|
+
import { getAppVersion, triggerUpgrade } from '../actions.js';
|
|
7
|
+
|
|
8
|
+
export function UpgradePage({ session }) {
|
|
9
|
+
const [version, setVersion] = useState('');
|
|
10
|
+
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
13
|
+
const [result, setResult] = useState(null); // 'success' | 'error'
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
getAppVersion()
|
|
17
|
+
.then(({ version, updateAvailable }) => {
|
|
18
|
+
setVersion(version);
|
|
19
|
+
setUpdateAvailable(updateAvailable);
|
|
20
|
+
})
|
|
21
|
+
.catch(() => {})
|
|
22
|
+
.finally(() => setLoading(false));
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleUpgrade = async () => {
|
|
26
|
+
setUpgrading(true);
|
|
27
|
+
setResult(null);
|
|
28
|
+
try {
|
|
29
|
+
await triggerUpgrade();
|
|
30
|
+
setResult('success');
|
|
31
|
+
} catch {
|
|
32
|
+
setResult('error');
|
|
33
|
+
} finally {
|
|
34
|
+
setUpgrading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<PageLayout session={session}>
|
|
40
|
+
<div className="flex items-center justify-between mb-6">
|
|
41
|
+
<h1 className="text-2xl font-semibold">Upgrade</h1>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{loading ? (
|
|
45
|
+
<div className="flex flex-col gap-4">
|
|
46
|
+
<div className="h-32 animate-pulse rounded-md bg-border/50" />
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
<div className="flex flex-col gap-6 max-w-lg">
|
|
50
|
+
{/* Version info card */}
|
|
51
|
+
<div className="rounded-md border bg-card p-6">
|
|
52
|
+
<div className="flex items-center gap-3 mb-4">
|
|
53
|
+
<ArrowUpCircleIcon size={24} />
|
|
54
|
+
<div>
|
|
55
|
+
<p className="text-sm text-muted-foreground">Installed version</p>
|
|
56
|
+
<p className="text-lg font-mono font-semibold">v{version}</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{updateAvailable ? (
|
|
61
|
+
<div className="rounded-md border border-blue-500/30 bg-blue-500/5 p-4 mb-4">
|
|
62
|
+
<p className="text-sm font-medium">
|
|
63
|
+
Version <span className="font-mono text-blue-500">v{updateAvailable}</span> is available
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
) : (
|
|
67
|
+
<div className="rounded-md border border-green-500/30 bg-green-500/5 p-4 mb-4">
|
|
68
|
+
<p className="text-sm font-medium text-green-600 dark:text-green-400">
|
|
69
|
+
You are on the latest version
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Upgrade button */}
|
|
75
|
+
{updateAvailable && (
|
|
76
|
+
<button
|
|
77
|
+
onClick={handleUpgrade}
|
|
78
|
+
disabled={upgrading || result === 'success'}
|
|
79
|
+
className="w-full inline-flex items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:pointer-events-none"
|
|
80
|
+
>
|
|
81
|
+
{upgrading ? (
|
|
82
|
+
<>
|
|
83
|
+
<SpinnerIcon size={16} />
|
|
84
|
+
Triggering upgrade...
|
|
85
|
+
</>
|
|
86
|
+
) : result === 'success' ? (
|
|
87
|
+
<>
|
|
88
|
+
<CheckIcon size={16} />
|
|
89
|
+
Upgrade triggered
|
|
90
|
+
</>
|
|
91
|
+
) : (
|
|
92
|
+
<>
|
|
93
|
+
<ArrowUpCircleIcon size={16} />
|
|
94
|
+
Upgrade to v{updateAvailable}
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
</button>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{/* Feedback */}
|
|
101
|
+
{result === 'success' && (
|
|
102
|
+
<p className="text-xs text-muted-foreground mt-3">
|
|
103
|
+
The upgrade workflow has been triggered. The server will update, rebuild, and reload automatically. This page will reflect the new version once the process completes.
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
{result === 'error' && (
|
|
107
|
+
<p className="text-xs text-red-500 mt-3">
|
|
108
|
+
Failed to trigger the upgrade workflow. Check that your GitHub token has workflow permissions.
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* What happens during upgrade */}
|
|
114
|
+
{updateAvailable && (
|
|
115
|
+
<div className="rounded-md border bg-card p-6">
|
|
116
|
+
<h2 className="text-sm font-medium mb-3">What happens during an upgrade</h2>
|
|
117
|
+
<ul className="text-sm text-muted-foreground space-y-2">
|
|
118
|
+
<li className="flex gap-2">
|
|
119
|
+
<span className="text-muted-foreground/60">1.</span>
|
|
120
|
+
<span>The <code className="text-xs bg-muted px-1 py-0.5 rounded">upgrade-event-handler</code> GitHub Actions workflow is triggered</span>
|
|
121
|
+
</li>
|
|
122
|
+
<li className="flex gap-2">
|
|
123
|
+
<span className="text-muted-foreground/60">2.</span>
|
|
124
|
+
<span>The package is updated via <code className="text-xs bg-muted px-1 py-0.5 rounded">npm update thepopebot</code></span>
|
|
125
|
+
</li>
|
|
126
|
+
<li className="flex gap-2">
|
|
127
|
+
<span className="text-muted-foreground/60">3.</span>
|
|
128
|
+
<span>A new build is created alongside the current one</span>
|
|
129
|
+
</li>
|
|
130
|
+
<li className="flex gap-2">
|
|
131
|
+
<span className="text-muted-foreground/60">4.</span>
|
|
132
|
+
<span>The builds are atomically swapped and the server reloads</span>
|
|
133
|
+
</li>
|
|
134
|
+
</ul>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</PageLayout>
|
|
140
|
+
);
|
|
141
|
+
}
|
package/lib/cron.js
CHANGED
|
@@ -1,8 +1,95 @@
|
|
|
1
1
|
import cron from 'node-cron';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
3
5
|
import { cronsFile, cronDir } from './paths.js';
|
|
4
6
|
import { executeAction } from './actions.js';
|
|
5
7
|
|
|
8
|
+
// Read installed version once at module load (doesn't change while server runs)
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const _installedVersion = JSON.parse(
|
|
12
|
+
fs.readFileSync(join(__dirname, '..', 'package.json'), 'utf8')
|
|
13
|
+
).version;
|
|
14
|
+
|
|
15
|
+
// In-memory flag for available update (read by sidebar, written by cron)
|
|
16
|
+
let _updateAvailable = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the in-memory update-available version (or null).
|
|
20
|
+
* @returns {string|null}
|
|
21
|
+
*/
|
|
22
|
+
function getUpdateAvailable() {
|
|
23
|
+
return _updateAvailable;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set the in-memory update-available version.
|
|
28
|
+
* @param {string|null} v
|
|
29
|
+
*/
|
|
30
|
+
function setUpdateAvailable(v) {
|
|
31
|
+
_updateAvailable = v;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compare two semver strings numerically.
|
|
36
|
+
* @param {string} candidate - e.g. "1.2.40"
|
|
37
|
+
* @param {string} baseline - e.g. "1.2.39"
|
|
38
|
+
* @returns {boolean} true if candidate > baseline
|
|
39
|
+
*/
|
|
40
|
+
function isVersionNewer(candidate, baseline) {
|
|
41
|
+
const a = candidate.split('.').map(Number);
|
|
42
|
+
const b = baseline.split('.').map(Number);
|
|
43
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
44
|
+
const av = a[i] || 0;
|
|
45
|
+
const bv = b[i] || 0;
|
|
46
|
+
if (av > bv) return true;
|
|
47
|
+
if (av < bv) return false;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check npm registry for a newer version of thepopebot.
|
|
54
|
+
*/
|
|
55
|
+
async function runVersionCheck() {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch('https://registry.npmjs.org/thepopebot/latest');
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
console.warn(`[version check] npm registry returned ${res.status}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
const latest = data.version;
|
|
64
|
+
|
|
65
|
+
if (isVersionNewer(latest, _installedVersion)) {
|
|
66
|
+
console.log(`[version check] update available: ${_installedVersion} → ${latest}`);
|
|
67
|
+
setUpdateAvailable(latest);
|
|
68
|
+
// Persist to DB
|
|
69
|
+
const { setAvailableVersion } = await import('./db/update-check.js');
|
|
70
|
+
setAvailableVersion(latest);
|
|
71
|
+
} else {
|
|
72
|
+
setUpdateAvailable(null);
|
|
73
|
+
// Clear DB
|
|
74
|
+
const { clearAvailableVersion } = await import('./db/update-check.js');
|
|
75
|
+
clearAvailableVersion();
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.warn(`[version check] failed: ${err.message}`);
|
|
79
|
+
// Leave existing flag untouched on error
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Start built-in crons (version check). Called from instrumentation.
|
|
85
|
+
*/
|
|
86
|
+
function startBuiltinCrons() {
|
|
87
|
+
// Schedule daily at midnight
|
|
88
|
+
cron.schedule('0 0 * * *', runVersionCheck);
|
|
89
|
+
// Run once immediately
|
|
90
|
+
runVersionCheck();
|
|
91
|
+
}
|
|
92
|
+
|
|
6
93
|
/**
|
|
7
94
|
* Load and schedule crons from CRONS.json
|
|
8
95
|
* @returns {Array} - Array of scheduled cron tasks
|
|
@@ -56,4 +143,4 @@ function loadCrons() {
|
|
|
56
143
|
return tasks;
|
|
57
144
|
}
|
|
58
145
|
|
|
59
|
-
export { loadCrons };
|
|
146
|
+
export { loadCrons, startBuiltinCrons, getUpdateAvailable, setUpdateAvailable };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { eq, and } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { settings } from './schema.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the stored available version from the DB.
|
|
8
|
+
* @returns {string|null}
|
|
9
|
+
*/
|
|
10
|
+
export function getAvailableVersion() {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const row = db
|
|
13
|
+
.select()
|
|
14
|
+
.from(settings)
|
|
15
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
|
|
16
|
+
.get();
|
|
17
|
+
|
|
18
|
+
return row ? row.value : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set the available version in the DB (delete + insert upsert).
|
|
23
|
+
* @param {string} version
|
|
24
|
+
*/
|
|
25
|
+
export function setAvailableVersion(version) {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
db.delete(settings)
|
|
28
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
|
|
29
|
+
.run();
|
|
30
|
+
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
db.insert(settings).values({
|
|
33
|
+
id: randomUUID(),
|
|
34
|
+
type: 'update',
|
|
35
|
+
key: 'available_version',
|
|
36
|
+
value: version,
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
}).run();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear the available version from the DB.
|
|
44
|
+
*/
|
|
45
|
+
export function clearAvailableVersion() {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
db.delete(settings)
|
|
48
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
|
|
49
|
+
.run();
|
|
50
|
+
}
|
package/lib/tools/github.js
CHANGED
|
@@ -255,6 +255,34 @@ async function rerunWorkflowRun(runId, failedOnly = false) {
|
|
|
255
255
|
return { success: true };
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Trigger a workflow via workflow_dispatch
|
|
260
|
+
* @param {string} workflowId - Workflow file name (e.g., 'upgrade-event-handler.yml')
|
|
261
|
+
* @param {string} [ref='main'] - Git ref to run the workflow on
|
|
262
|
+
* @param {object} [inputs={}] - Workflow inputs
|
|
263
|
+
*/
|
|
264
|
+
async function triggerWorkflowDispatch(workflowId, ref = 'main', inputs = {}) {
|
|
265
|
+
const { GH_OWNER, GH_REPO } = process.env;
|
|
266
|
+
const res = await fetch(
|
|
267
|
+
`https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/actions/workflows/${workflowId}/dispatches`,
|
|
268
|
+
{
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: {
|
|
271
|
+
'Authorization': `Bearer ${process.env.GH_TOKEN}`,
|
|
272
|
+
'Accept': 'application/vnd.github+json',
|
|
273
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
274
|
+
'Content-Type': 'application/json',
|
|
275
|
+
},
|
|
276
|
+
body: JSON.stringify({ ref, inputs }),
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
if (!res.ok && res.status !== 204) {
|
|
280
|
+
const error = await res.text();
|
|
281
|
+
throw new Error(`GitHub API error: ${res.status} ${error}`);
|
|
282
|
+
}
|
|
283
|
+
return { success: true };
|
|
284
|
+
}
|
|
285
|
+
|
|
258
286
|
export {
|
|
259
287
|
githubApi,
|
|
260
288
|
getWorkflowRuns,
|
|
@@ -263,4 +291,5 @@ export {
|
|
|
263
291
|
getSwarmStatus,
|
|
264
292
|
cancelWorkflowRun,
|
|
265
293
|
rerunWorkflowRun,
|
|
294
|
+
triggerWorkflowDispatch,
|
|
266
295
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Upgrade Event Handler
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
concurrency:
|
|
7
|
+
group: deploy
|
|
8
|
+
cancel-in-progress: true
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
upgrade:
|
|
12
|
+
runs-on: self-hosted
|
|
13
|
+
steps:
|
|
14
|
+
- name: Upgrade thepopebot
|
|
15
|
+
run: |
|
|
16
|
+
docker exec thepopebot-event-handler bash -c '
|
|
17
|
+
export GH_TOKEN=$(grep "^GH_TOKEN=" /app/.env | cut -d= -f2-)
|
|
18
|
+
echo "${GH_TOKEN}" | gh auth login --with-token
|
|
19
|
+
gh auth setup-git
|
|
20
|
+
|
|
21
|
+
git fetch origin main
|
|
22
|
+
git reset --hard origin/main
|
|
23
|
+
|
|
24
|
+
# Update thepopebot package
|
|
25
|
+
npm update thepopebot
|
|
26
|
+
npm install --omit=dev
|
|
27
|
+
|
|
28
|
+
# Atomic swap build (same pattern as rebuild-event-handler)
|
|
29
|
+
rm -rf .next-new .next-old
|
|
30
|
+
NEXT_BUILD_DIR=.next-new npm run build
|
|
31
|
+
|
|
32
|
+
mv .next .next-old 2>/dev/null || true
|
|
33
|
+
mv .next-new .next
|
|
34
|
+
|
|
35
|
+
echo "Upgrade complete, reloading..."
|
|
36
|
+
npx pm2 reload all
|
|
37
|
+
|
|
38
|
+
rm -rf .next-old
|
|
39
|
+
'
|