@promptbook/cli 0.112.0-96 → 0.112.0-98
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/apps/agents-server/playwright.config.ts +2 -1
- package/apps/agents-server/src/app/admin/cli-access/CliAccessClient.tsx +99 -0
- package/apps/agents-server/src/app/admin/cli-access/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +124 -34
- package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
- package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
- package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
- package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
- package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +24 -0
- package/apps/agents-server/src/app/api/admin/cli-access/route.ts +137 -0
- package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +140 -0
- package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
- package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +7 -2
- package/apps/agents-server/src/app/api/admin/servers/route.ts +95 -4
- package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
- package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
- package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
- package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +63 -0
- package/apps/agents-server/src/app/page.tsx +10 -0
- package/apps/agents-server/src/components/AdminTerminal/AdminTerminalCard.tsx +279 -0
- package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +336 -0
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
- package/apps/agents-server/src/languages/translations/english.yaml +2 -0
- package/apps/agents-server/src/middleware.ts +32 -0
- package/apps/agents-server/src/tools/BrowserConnectionProvider.ts +1 -1
- package/apps/agents-server/src/utils/chatExport/downloadChatPdfFromServer.ts +59 -0
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +37 -0
- package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +234 -0
- package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
- package/apps/agents-server/src/utils/createInteractiveTerminalEventStream.ts +84 -0
- package/apps/agents-server/src/utils/interactiveTerminalSession.ts +442 -0
- package/apps/agents-server/src/utils/serverCliAccess.ts +221 -0
- package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
- package/apps/agents-server/src/utils/serverRegistry.ts +3 -2
- package/apps/agents-server/src/utils/session.ts +37 -9
- package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
- package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
- package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
- package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
- package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -13
- package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
- package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
- package/esm/index.es.js +7 -5
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/esm/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/esm/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/Chat.tsx +2 -0
- package/src/book-components/Chat/Chat/ChatActionsBar.tsx +17 -9
- package/src/book-components/Chat/Chat/ChatProps.tsx +7 -0
- package/src/book-components/Chat/save/_common/ChatSaveFormatHandler.ts +40 -0
- package/src/book-components/Chat/save/_common/createChatExportFilename.ts +20 -0
- package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
- package/src/other/templates/getTemplatesPipelineCollection.ts +718 -790
- package/src/scrapers/document/DocumentScraper.ts +1 -1
- package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
- package/umd/index.umd.js +7 -5
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/Chat/ChatActionsBar.d.ts +2 -0
- package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/umd/src/book-components/Chat/save/_common/ChatSaveFormatHandler.d.ts +35 -0
- package/umd/src/book-components/Chat/save/_common/createChatExportFilename.d.ts +11 -0
- package/umd/src/version.d.ts +1 -1
- package/src/conversion/validation/_importPipeline.ts +0 -88
- /package/esm/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
- /package/umd/src/conversion/validation/{_importPipeline.d.ts → _importPipeline.test.d.ts} +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CheckCircle2, Download, Loader2, RefreshCcw, Rocket, Server, TriangleAlert } from 'lucide-react';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { Card } from '../../../components/Homepage/Card';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Browser-safe environment option returned by the update API.
|
|
9
|
+
*/
|
|
10
|
+
type UpdateEnvironmentOption = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly branch: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly description: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Browser-safe latest update-job snapshot.
|
|
19
|
+
*/
|
|
20
|
+
type UpdateJobSnapshot = {
|
|
21
|
+
readonly status: 'idle' | 'running' | 'succeeded' | 'failed';
|
|
22
|
+
readonly pid: number | null;
|
|
23
|
+
readonly targetBranch: string | null;
|
|
24
|
+
readonly targetEnvironment: UpdateEnvironmentOption;
|
|
25
|
+
readonly currentStep: string | null;
|
|
26
|
+
readonly currentCommitSha: string | null;
|
|
27
|
+
readonly targetCommitSha: string | null;
|
|
28
|
+
readonly errorMessage: string | null;
|
|
29
|
+
readonly startedAt: string | null;
|
|
30
|
+
readonly finishedAt: string | null;
|
|
31
|
+
readonly isStale: boolean;
|
|
32
|
+
readonly logTail: string | null;
|
|
33
|
+
readonly logFilePath: string | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Browser-safe self-update overview returned by the super-admin API.
|
|
38
|
+
*/
|
|
39
|
+
type UpdateOverview = {
|
|
40
|
+
readonly isAvailable: boolean;
|
|
41
|
+
readonly unavailableReason: string | null;
|
|
42
|
+
readonly environments: ReadonlyArray<UpdateEnvironmentOption>;
|
|
43
|
+
readonly currentEnvironment: UpdateEnvironmentOption;
|
|
44
|
+
readonly repositoryDirectory: string | null;
|
|
45
|
+
readonly currentCommitSha: string | null;
|
|
46
|
+
readonly currentCommitShortSha: string | null;
|
|
47
|
+
readonly currentCommitMessage: string | null;
|
|
48
|
+
readonly latestRemoteCommitSha: string | null;
|
|
49
|
+
readonly latestRemoteCommitShortSha: string | null;
|
|
50
|
+
readonly isUpdateAvailable: boolean;
|
|
51
|
+
readonly job: UpdateJobSnapshot;
|
|
52
|
+
readonly error?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Client UI for standalone VPS branch-aware self-updates.
|
|
57
|
+
*/
|
|
58
|
+
export function UpdateClient() {
|
|
59
|
+
const [overview, setOverview] = useState<UpdateOverview | null>(null);
|
|
60
|
+
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>('');
|
|
61
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
62
|
+
const [isStartingUpdate, setIsStartingUpdate] = useState(false);
|
|
63
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
64
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loads the latest self-update overview from the server.
|
|
68
|
+
*/
|
|
69
|
+
const loadOverview = useCallback(async (options?: { readonly isSilent?: boolean }): Promise<void> => {
|
|
70
|
+
try {
|
|
71
|
+
if (!options?.isSilent) {
|
|
72
|
+
setIsLoading(true);
|
|
73
|
+
}
|
|
74
|
+
setErrorMessage(null);
|
|
75
|
+
|
|
76
|
+
const response = await fetch('/api/admin/update', { cache: 'no-store' });
|
|
77
|
+
const payload = (await response.json()) as UpdateOverview;
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(payload.error || 'Failed to load the update overview.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setOverview(payload);
|
|
84
|
+
setSelectedEnvironmentId((currentSelectedEnvironmentId) =>
|
|
85
|
+
currentSelectedEnvironmentId || payload.currentEnvironment.id,
|
|
86
|
+
);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load the update overview.');
|
|
89
|
+
} finally {
|
|
90
|
+
if (!options?.isSilent) {
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
void loadOverview();
|
|
98
|
+
}, [loadOverview]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (overview?.job.status !== 'running') {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const interval = window.setInterval(() => {
|
|
106
|
+
void loadOverview({ isSilent: true });
|
|
107
|
+
}, 4000);
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
window.clearInterval(interval);
|
|
111
|
+
};
|
|
112
|
+
}, [loadOverview, overview?.job.status]);
|
|
113
|
+
|
|
114
|
+
const selectedEnvironment = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
overview?.environments.find((environment) => environment.id === selectedEnvironmentId) ||
|
|
117
|
+
overview?.currentEnvironment ||
|
|
118
|
+
null,
|
|
119
|
+
[overview, selectedEnvironmentId],
|
|
120
|
+
);
|
|
121
|
+
const isEnvironmentSwitchRequired =
|
|
122
|
+
Boolean(selectedEnvironment) && selectedEnvironment?.id !== overview?.currentEnvironment.id;
|
|
123
|
+
const isUpdateRunning = overview?.job.status === 'running';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Starts one detached update run for the selected environment.
|
|
127
|
+
*/
|
|
128
|
+
async function startUpdate(): Promise<void> {
|
|
129
|
+
if (!selectedEnvironment) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
setIsStartingUpdate(true);
|
|
135
|
+
setErrorMessage(null);
|
|
136
|
+
setSuccessMessage(null);
|
|
137
|
+
|
|
138
|
+
const response = await fetch('/api/admin/update', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
environment: selectedEnvironment.id,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
const payload = (await response.json()) as UpdateOverview;
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(payload.error || 'Failed to start the update.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setOverview(payload);
|
|
154
|
+
setSuccessMessage(
|
|
155
|
+
isEnvironmentSwitchRequired
|
|
156
|
+
? `Switched to ${selectedEnvironment.label} and started the standalone VPS update.`
|
|
157
|
+
: 'Standalone VPS update started.',
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to start the update.');
|
|
161
|
+
} finally {
|
|
162
|
+
setIsStartingUpdate(false);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="container mx-auto space-y-6 px-4 py-8">
|
|
168
|
+
<div className="mt-20 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
|
169
|
+
<div>
|
|
170
|
+
<h1 className="text-3xl font-light text-gray-900">Update</h1>
|
|
171
|
+
<p className="mt-1 max-w-3xl text-sm text-gray-500">
|
|
172
|
+
Switch the standalone VPS between Production, Live, Preview, and LTS, and update the managed
|
|
173
|
+
Promptbook checkout with one click.
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={() => void loadOverview()}
|
|
180
|
+
disabled={isLoading || isStartingUpdate}
|
|
181
|
+
className="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
182
|
+
>
|
|
183
|
+
<RefreshCcw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
184
|
+
Refresh
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{errorMessage && (
|
|
189
|
+
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
190
|
+
{errorMessage}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
{successMessage && (
|
|
194
|
+
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
|
|
195
|
+
{successMessage}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{!overview?.isAvailable && overview?.unavailableReason && (
|
|
200
|
+
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
|
201
|
+
{overview.unavailableReason}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<div className="grid gap-4 lg:grid-cols-[1.4fr_1fr]">
|
|
206
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
207
|
+
<div className="space-y-4">
|
|
208
|
+
<div className="flex items-start gap-3">
|
|
209
|
+
<Server className="mt-0.5 h-5 w-5 text-blue-600" />
|
|
210
|
+
<div>
|
|
211
|
+
<h2 className="text-lg font-semibold text-slate-900">Current deployment</h2>
|
|
212
|
+
<p className="mt-1 text-sm text-slate-500">
|
|
213
|
+
The server currently tracks the <span className="font-medium">{overview?.currentEnvironment.label || 'Production'}</span>{' '}
|
|
214
|
+
environment on branch <span className="font-mono">{overview?.currentEnvironment.branch || 'production'}</span>.
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<dl className="grid gap-4 text-sm text-slate-600 sm:grid-cols-2">
|
|
220
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
221
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</dt>
|
|
222
|
+
<dd className="mt-1 font-mono text-slate-900">
|
|
223
|
+
{overview?.currentEnvironment.branch || 'production'}
|
|
224
|
+
</dd>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
227
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
228
|
+
Deployed commit
|
|
229
|
+
</dt>
|
|
230
|
+
<dd className="mt-1 font-mono text-slate-900">
|
|
231
|
+
{overview?.currentCommitShortSha || 'Unknown'}
|
|
232
|
+
</dd>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
235
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
236
|
+
Latest remote commit
|
|
237
|
+
</dt>
|
|
238
|
+
<dd className="mt-1 font-mono text-slate-900">
|
|
239
|
+
{overview?.latestRemoteCommitShortSha || 'Unknown'}
|
|
240
|
+
</dd>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
243
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
244
|
+
Update availability
|
|
245
|
+
</dt>
|
|
246
|
+
<dd className="mt-1 flex items-center gap-2 text-slate-900">
|
|
247
|
+
{overview?.isUpdateAvailable ? (
|
|
248
|
+
<>
|
|
249
|
+
<TriangleAlert className="h-4 w-4 text-amber-500" />
|
|
250
|
+
New commit available
|
|
251
|
+
</>
|
|
252
|
+
) : (
|
|
253
|
+
<>
|
|
254
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
|
|
255
|
+
Up to date
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
258
|
+
</dd>
|
|
259
|
+
</div>
|
|
260
|
+
</dl>
|
|
261
|
+
|
|
262
|
+
{overview?.currentCommitMessage && (
|
|
263
|
+
<div className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-600">
|
|
264
|
+
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
265
|
+
Current commit message
|
|
266
|
+
</div>
|
|
267
|
+
<div className="mt-1 text-slate-900">{overview.currentCommitMessage}</div>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{overview?.repositoryDirectory && (
|
|
272
|
+
<div className="text-xs text-slate-500">
|
|
273
|
+
Managed repository:
|
|
274
|
+
<span className="ml-2 font-mono text-slate-700">{overview.repositoryDirectory}</span>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
</Card>
|
|
279
|
+
|
|
280
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
281
|
+
<div className="space-y-4">
|
|
282
|
+
<div>
|
|
283
|
+
<h2 className="text-lg font-semibold text-slate-900">Target environment</h2>
|
|
284
|
+
<p className="mt-1 text-sm text-slate-500">
|
|
285
|
+
Selecting another environment automatically updates the server to the latest commit on
|
|
286
|
+
that branch.
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className="grid gap-3">
|
|
291
|
+
{overview?.environments.map((environment) => {
|
|
292
|
+
const isSelected = environment.id === selectedEnvironment?.id;
|
|
293
|
+
const isCurrent = environment.id === overview.currentEnvironment.id;
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<button
|
|
297
|
+
key={environment.id}
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={() => setSelectedEnvironmentId(environment.id)}
|
|
300
|
+
disabled={isUpdateRunning || isStartingUpdate}
|
|
301
|
+
className={`rounded-2xl border px-4 py-4 text-left transition ${
|
|
302
|
+
isSelected
|
|
303
|
+
? 'border-blue-300 bg-blue-50 text-blue-900 shadow-sm'
|
|
304
|
+
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:shadow-sm'
|
|
305
|
+
} disabled:cursor-not-allowed disabled:opacity-60`}
|
|
306
|
+
>
|
|
307
|
+
<div className="flex items-center justify-between gap-3">
|
|
308
|
+
<div className="text-sm font-semibold">{environment.label}</div>
|
|
309
|
+
{isCurrent && (
|
|
310
|
+
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-700">
|
|
311
|
+
Current
|
|
312
|
+
</span>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
<div className="mt-1 font-mono text-xs">{environment.branch}</div>
|
|
316
|
+
<div className="mt-2 text-sm opacity-80">{environment.description}</div>
|
|
317
|
+
</button>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<button
|
|
323
|
+
type="button"
|
|
324
|
+
onClick={() => void startUpdate()}
|
|
325
|
+
disabled={
|
|
326
|
+
!overview?.isAvailable ||
|
|
327
|
+
!selectedEnvironment ||
|
|
328
|
+
isUpdateRunning ||
|
|
329
|
+
isStartingUpdate ||
|
|
330
|
+
(!isEnvironmentSwitchRequired && !overview?.isUpdateAvailable)
|
|
331
|
+
}
|
|
332
|
+
className="inline-flex w-full items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-3 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
333
|
+
>
|
|
334
|
+
{isStartingUpdate || isUpdateRunning ? (
|
|
335
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
336
|
+
) : isEnvironmentSwitchRequired ? (
|
|
337
|
+
<Rocket className="h-4 w-4" />
|
|
338
|
+
) : (
|
|
339
|
+
<Download className="h-4 w-4" />
|
|
340
|
+
)}
|
|
341
|
+
{isEnvironmentSwitchRequired
|
|
342
|
+
? `Switch to ${selectedEnvironment?.label || 'selected environment'} and update`
|
|
343
|
+
: 'Update to latest commit'}
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
</Card>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
350
|
+
<div className="space-y-4">
|
|
351
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
352
|
+
<div>
|
|
353
|
+
<h2 className="text-lg font-semibold text-slate-900">Update job</h2>
|
|
354
|
+
<p className="mt-1 text-sm text-slate-500">
|
|
355
|
+
The update runs in the background so the browser request can finish cleanly before pm2
|
|
356
|
+
restarts the server.
|
|
357
|
+
</p>
|
|
358
|
+
</div>
|
|
359
|
+
<span
|
|
360
|
+
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wide ${
|
|
361
|
+
overview?.job.status === 'running'
|
|
362
|
+
? 'border-blue-200 bg-blue-50 text-blue-700'
|
|
363
|
+
: overview?.job.status === 'failed'
|
|
364
|
+
? 'border-rose-200 bg-rose-50 text-rose-700'
|
|
365
|
+
: overview?.job.status === 'succeeded'
|
|
366
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
367
|
+
: 'border-slate-200 bg-slate-50 text-slate-500'
|
|
368
|
+
}`}
|
|
369
|
+
>
|
|
370
|
+
{overview?.job.status || 'idle'}
|
|
371
|
+
</span>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<dl className="grid gap-4 text-sm text-slate-600 md:grid-cols-2 xl:grid-cols-4">
|
|
375
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
376
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Target</dt>
|
|
377
|
+
<dd className="mt-1 text-slate-900">{overview?.job.targetEnvironment.label || 'Production'}</dd>
|
|
378
|
+
</div>
|
|
379
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
380
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Step</dt>
|
|
381
|
+
<dd className="mt-1 text-slate-900">{overview?.job.currentStep || 'Idle'}</dd>
|
|
382
|
+
</div>
|
|
383
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
384
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Started</dt>
|
|
385
|
+
<dd className="mt-1 text-slate-900">{formatTimestamp(overview?.job.startedAt)}</dd>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
|
|
388
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Finished</dt>
|
|
389
|
+
<dd className="mt-1 text-slate-900">{formatTimestamp(overview?.job.finishedAt)}</dd>
|
|
390
|
+
</div>
|
|
391
|
+
</dl>
|
|
392
|
+
|
|
393
|
+
{overview?.job.errorMessage && (
|
|
394
|
+
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
395
|
+
{overview.job.errorMessage}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
{overview?.job.isStale && (
|
|
399
|
+
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
|
400
|
+
The previous background update process stopped unexpectedly. You can start the update again.
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
{overview?.job.logFilePath && (
|
|
404
|
+
<div className="text-xs text-slate-500">
|
|
405
|
+
Installer log:
|
|
406
|
+
<span className="ml-2 font-mono text-slate-700">{overview.job.logFilePath}</span>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
<pre className="max-h-[28rem] overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
|
|
410
|
+
{overview?.job.logTail || 'No persisted update log output yet.'}
|
|
411
|
+
</pre>
|
|
412
|
+
</div>
|
|
413
|
+
</Card>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Formats optional timestamps for the status cards.
|
|
420
|
+
*
|
|
421
|
+
* @param value - ISO timestamp or `null`.
|
|
422
|
+
* @returns Human-friendly timestamp or fallback text.
|
|
423
|
+
*/
|
|
424
|
+
function formatTimestamp(value: string | null | undefined): string {
|
|
425
|
+
if (!value) {
|
|
426
|
+
return 'Not available';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const date = new Date(value);
|
|
430
|
+
if (Number.isNaN(date.getTime())) {
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return `${date.toLocaleString()} (${value})`;
|
|
435
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
+
import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
|
|
3
|
+
import { UpdateClient } from './UpdateClient';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Super-admin page for branch-aware standalone VPS self-updates.
|
|
7
|
+
*/
|
|
8
|
+
export default async function UpdatePage() {
|
|
9
|
+
if (!(await isUserGlobalAdmin())) {
|
|
10
|
+
return <ForbiddenPage />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <UpdateClient />;
|
|
14
|
+
}
|
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
import { Chat } from '@promptbook-local/components';
|
|
4
4
|
import { useCallback, useMemo, type CSSProperties, type ReactNode } from 'react';
|
|
5
5
|
import type { ChatParticipant } from '../../../../../../../src/book-components/Chat/types/ChatParticipant';
|
|
6
|
+
import type { ChatSaveFormatHandlerOptions } from '../../../../../../../src/book-components/Chat/save/_common/ChatSaveFormatHandler';
|
|
6
7
|
import { useAgentBackground } from '../../../../components/AgentProfile/useAgentBackground';
|
|
7
8
|
import { useChatEnterBehaviorPreferences } from '../../../../components/ChatEnterBehavior/ChatEnterBehaviorPreferencesProvider';
|
|
9
|
+
import { notifyError } from '../../../../components/Notifications/notifications';
|
|
8
10
|
import { useChatVisualMode } from '../../../../components/ChatVisualMode/ChatVisualModeProvider';
|
|
9
11
|
import { useServerLanguage } from '../../../../components/ServerLanguage/ServerLanguageProvider';
|
|
10
12
|
import { ChatThreadLoadingSkeleton } from '../../../../components/Skeleton/ChatThreadLoadingSkeleton';
|
|
11
13
|
import { useSoundSystem } from '../../../../components/SoundSystemProvider/SoundSystemProvider';
|
|
12
14
|
import { usePromptbookTheme } from '../../../../components/ThemeMode/usePromptbookTheme';
|
|
13
15
|
import { createDefaultChatEffects } from '../../../../utils/chat/createDefaultChatEffects';
|
|
16
|
+
import { downloadChatPdfFromServer } from '../../../../utils/chatExport/downloadChatPdfFromServer';
|
|
14
17
|
import { executeQuickActionButton } from '../../../../utils/chat/executeQuickActionButton';
|
|
15
18
|
import {
|
|
16
19
|
isChatFeedbackEnabled,
|
|
@@ -175,6 +178,26 @@ export function CanonicalAgentChatSurface({
|
|
|
175
178
|
</button>
|
|
176
179
|
);
|
|
177
180
|
}, [cancellableJob, isReadOnly, onCancelActiveJob, translateText]);
|
|
181
|
+
const handlePdfSaveFormat = useCallback(
|
|
182
|
+
async ({ title, messages, participants }: ChatSaveFormatHandlerOptions) => {
|
|
183
|
+
try {
|
|
184
|
+
await downloadChatPdfFromServer({
|
|
185
|
+
title,
|
|
186
|
+
messages,
|
|
187
|
+
participants,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
notifyError(error instanceof Error ? error.message : 'Failed to export chat as PDF.');
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
[],
|
|
194
|
+
);
|
|
195
|
+
const saveFormatHandlers = useMemo(
|
|
196
|
+
() => ({
|
|
197
|
+
pdf: handlePdfSaveFormat,
|
|
198
|
+
}),
|
|
199
|
+
[handlePdfSaveFormat],
|
|
200
|
+
);
|
|
178
201
|
const extraActionNodes = useMemo(
|
|
179
202
|
() => (
|
|
180
203
|
<>
|
|
@@ -235,6 +258,7 @@ export function CanonicalAgentChatSurface({
|
|
|
235
258
|
elevenLabsVoiceId={state.elevenLabsVoiceId}
|
|
236
259
|
teamAgentProfiles={state.teamAgentProfiles}
|
|
237
260
|
extraActions={extraActionNodes}
|
|
261
|
+
saveFormatHandlers={saveFormatHandlers}
|
|
238
262
|
theme={promptbookTheme}
|
|
239
263
|
>
|
|
240
264
|
{isReadOnly && frozenChatBannerLabel && (
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
3
|
+
import { createInteractiveTerminalEventStream } from '@/src/utils/createInteractiveTerminalEventStream';
|
|
4
|
+
import {
|
|
5
|
+
getLatestServerCliAccessSession,
|
|
6
|
+
getServerCliAccessSession,
|
|
7
|
+
startServerCliAccessSession,
|
|
8
|
+
stopServerCliAccessSession,
|
|
9
|
+
subscribeToServerCliAccessSession,
|
|
10
|
+
writeServerCliAccessSessionInput,
|
|
11
|
+
} from '@/src/utils/serverCliAccess';
|
|
12
|
+
|
|
13
|
+
export const runtime = 'nodejs';
|
|
14
|
+
export const dynamic = 'force-dynamic';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads the latest CLI access session or streams a specific terminal session.
|
|
18
|
+
*/
|
|
19
|
+
export async function GET(request: Request) {
|
|
20
|
+
if (!(await isUserGlobalAdmin())) {
|
|
21
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { searchParams } = new URL(request.url);
|
|
26
|
+
const sessionId = searchParams.get('sessionId')?.trim() || '';
|
|
27
|
+
const isStreamRequested = searchParams.get('stream') === '1';
|
|
28
|
+
|
|
29
|
+
if (isStreamRequested) {
|
|
30
|
+
if (!sessionId) {
|
|
31
|
+
return NextResponse.json({ error: 'CLI access session id is required.' }, { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const session = getServerCliAccessSession(sessionId);
|
|
35
|
+
if (!session) {
|
|
36
|
+
return NextResponse.json({ error: 'CLI access session was not found.' }, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return createInteractiveTerminalEventStream(
|
|
40
|
+
request,
|
|
41
|
+
sessionId,
|
|
42
|
+
session,
|
|
43
|
+
subscribeToServerCliAccessSession,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
session: getLatestServerCliAccessSession(),
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: error instanceof Error ? error.message : 'Failed to load the CLI access session.' },
|
|
53
|
+
{ status: 500 },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Starts or reconnects to the raw server shell exposed in the browser.
|
|
60
|
+
*/
|
|
61
|
+
export async function POST() {
|
|
62
|
+
if (!(await isUserGlobalAdmin())) {
|
|
63
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return NextResponse.json({
|
|
68
|
+
session: await startServerCliAccessSession(),
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ error: error instanceof Error ? error.message : 'Failed to start the CLI access session.' },
|
|
73
|
+
{ status: 500 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sends raw input to the running CLI access shell.
|
|
80
|
+
*/
|
|
81
|
+
export async function PATCH(request: Request) {
|
|
82
|
+
if (!(await isUserGlobalAdmin())) {
|
|
83
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const body = (await request.json().catch(() => null)) as
|
|
88
|
+
| {
|
|
89
|
+
readonly sessionId?: string;
|
|
90
|
+
readonly input?: string;
|
|
91
|
+
}
|
|
92
|
+
| null;
|
|
93
|
+
|
|
94
|
+
if (!body?.sessionId || typeof body.input !== 'string') {
|
|
95
|
+
return NextResponse.json({ error: 'CLI access session input is required.' }, { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
session: writeServerCliAccessSessionInput(body.sessionId, body.input),
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ error: error instanceof Error ? error.message : 'Failed to send CLI access input.' },
|
|
104
|
+
{ status: 500 },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stops one running CLI access shell session.
|
|
111
|
+
*/
|
|
112
|
+
export async function DELETE(request: Request) {
|
|
113
|
+
if (!(await isUserGlobalAdmin())) {
|
|
114
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const body = (await request.json().catch(() => null)) as
|
|
119
|
+
| {
|
|
120
|
+
readonly sessionId?: string;
|
|
121
|
+
}
|
|
122
|
+
| null;
|
|
123
|
+
|
|
124
|
+
if (!body?.sessionId) {
|
|
125
|
+
return NextResponse.json({ error: 'CLI access session id is required.' }, { status: 400 });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return NextResponse.json({
|
|
129
|
+
session: stopServerCliAccessSession(body.sessionId),
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return NextResponse.json(
|
|
133
|
+
{ error: error instanceof Error ? error.message : 'Failed to stop the CLI access session.' },
|
|
134
|
+
{ status: 500 },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|