@promptbook/cli 0.112.0-93 → 0.112.0-95
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/src/app/admin/_components/AdminConfigurationShell.tsx +13 -7
- package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +225 -0
- package/apps/agents-server/src/app/admin/code-runners/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/environment/EnvironmentVariablesClient.tsx +259 -0
- package/apps/agents-server/src/app/admin/environment/page.tsx +21 -0
- package/apps/agents-server/src/app/admin/logs/LogsClient.tsx +78 -0
- package/apps/agents-server/src/app/admin/logs/page.tsx +14 -0
- package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +64 -33
- package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +15 -2
- package/apps/agents-server/src/app/admin/servers/page.tsx +3 -3
- package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +12 -2
- package/apps/agents-server/src/app/api/admin/code-runners/route.ts +104 -0
- package/apps/agents-server/src/app/api/admin/environment/route.ts +65 -0
- package/apps/agents-server/src/app/api/admin/logs/route.ts +24 -0
- package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +79 -3
- package/apps/agents-server/src/app/api/admin/servers/route.ts +36 -1
- package/apps/agents-server/src/app/page.tsx +101 -1
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +23 -4
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +4 -0
- package/apps/agents-server/src/languages/translations/english.yaml +4 -0
- package/apps/agents-server/src/tools/$provideServer.ts +27 -0
- package/apps/agents-server/src/utils/serverRegistry.ts +20 -1
- package/apps/agents-server/src/utils/session.ts +123 -2
- package/apps/agents-server/src/utils/vpsConfiguration.ts +550 -0
- package/esm/index.es.js +1 -1
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +2 -1
- package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +9 -398
- package/src/book-components/Chat/utils/renderMarkdown.ts +323 -8
- package/src/other/templates/getTemplatesPipelineCollection.ts +683 -879
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +1 -1
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/utils/renderMarkdown.test.d.ts +1 -0
- package/umd/src/version.d.ts +1 -1
|
@@ -6,7 +6,7 @@ import Link from 'next/link';
|
|
|
6
6
|
*
|
|
7
7
|
* @private admin configuration UI helper
|
|
8
8
|
*/
|
|
9
|
-
type AdminConfigurationPage = 'metadata' | 'limits';
|
|
9
|
+
type AdminConfigurationPage = 'environment' | 'metadata' | 'limits';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* One shared navigation item rendered in the configuration shell.
|
|
@@ -15,15 +15,21 @@ type AdminConfigurationPage = 'metadata' | 'limits';
|
|
|
15
15
|
*/
|
|
16
16
|
const ADMIN_CONFIGURATION_NAVIGATION_ITEMS: ReadonlyArray<{
|
|
17
17
|
readonly id: AdminConfigurationPage;
|
|
18
|
-
readonly href: '/admin/metadata' | '/admin/limits';
|
|
18
|
+
readonly href: '/admin/environment' | '/admin/metadata' | '/admin/limits';
|
|
19
19
|
readonly label: string;
|
|
20
20
|
readonly description: string;
|
|
21
21
|
}> = [
|
|
22
|
+
{
|
|
23
|
+
id: 'environment',
|
|
24
|
+
href: '/admin/environment',
|
|
25
|
+
label: 'Environment variables',
|
|
26
|
+
description: 'VPS-wide .env values with secrets masked in the browser.',
|
|
27
|
+
},
|
|
22
28
|
{
|
|
23
29
|
id: 'metadata',
|
|
24
30
|
href: '/admin/metadata',
|
|
25
31
|
label: 'Metadata',
|
|
26
|
-
description: '
|
|
32
|
+
description: 'Domain-specific feature flags, text settings, and compatibility keys.',
|
|
27
33
|
},
|
|
28
34
|
{
|
|
29
35
|
id: 'limits',
|
|
@@ -44,7 +50,7 @@ type AdminConfigurationShellProps = {
|
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
|
-
* Shared page shell used by the Metadata and Limits admin pages.
|
|
53
|
+
* Shared page shell used by the Environment variables, Metadata, and Limits admin pages.
|
|
48
54
|
*/
|
|
49
55
|
export function AdminConfigurationShell({ activePage, children }: AdminConfigurationShellProps) {
|
|
50
56
|
return (
|
|
@@ -53,11 +59,11 @@ export function AdminConfigurationShell({ activePage, children }: AdminConfigura
|
|
|
53
59
|
<div>
|
|
54
60
|
<h1 className="text-3xl font-light text-gray-900">Server configuration</h1>
|
|
55
61
|
<p className="mt-1 max-w-3xl text-sm text-gray-500">
|
|
56
|
-
Manage
|
|
57
|
-
|
|
62
|
+
Manage VPS-wide environment variables, domain-specific metadata, and dedicated operational
|
|
63
|
+
limits from one configuration area.
|
|
58
64
|
</p>
|
|
59
65
|
</div>
|
|
60
|
-
<div className="grid gap-3 md:grid-cols-
|
|
66
|
+
<div className="grid gap-3 md:grid-cols-3">
|
|
61
67
|
{ADMIN_CONFIGURATION_NAVIGATION_ITEMS.map((item) => {
|
|
62
68
|
const isActive = item.id === activePage;
|
|
63
69
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, Save, ServerCog } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { Card } from '../../../components/Homepage/Card';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Code-runner API response.
|
|
9
|
+
*/
|
|
10
|
+
type CodeRunnersResponse = {
|
|
11
|
+
readonly agent?: string;
|
|
12
|
+
readonly model?: string;
|
|
13
|
+
readonly thinkingLevel?: string;
|
|
14
|
+
readonly status?: string;
|
|
15
|
+
readonly applyResult?: {
|
|
16
|
+
readonly isAvailable: boolean;
|
|
17
|
+
readonly output: string;
|
|
18
|
+
} | null;
|
|
19
|
+
readonly error?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported runner options shown by the standalone UI.
|
|
24
|
+
*/
|
|
25
|
+
const RUNNER_OPTIONS = [
|
|
26
|
+
{ value: 'github-copilot', label: 'GitHub Copilot' },
|
|
27
|
+
{ value: 'openai-codex', label: 'OpenAI Codex' },
|
|
28
|
+
{ value: 'claude-code', label: 'Claude Code' },
|
|
29
|
+
{ value: 'opencode', label: 'Opencode' },
|
|
30
|
+
{ value: 'gemini', label: 'Gemini' },
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shared input styling for code-runner controls.
|
|
35
|
+
*/
|
|
36
|
+
const INPUT_CLASS_NAME =
|
|
37
|
+
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:bg-gray-50 disabled:text-gray-500';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Client UI for configuring the local coding runner used by durable chats.
|
|
41
|
+
*/
|
|
42
|
+
export function CodeRunnersClient() {
|
|
43
|
+
const [agent, setAgent] = useState('github-copilot');
|
|
44
|
+
const [model, setModel] = useState('gpt-5.4');
|
|
45
|
+
const [thinkingLevel, setThinkingLevel] = useState('xhigh');
|
|
46
|
+
const [status, setStatus] = useState('');
|
|
47
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
48
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
49
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
50
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
51
|
+
const [applyOutput, setApplyOutput] = useState<string | null>(null);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
void loadConfiguration();
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Loads current code-runner settings.
|
|
59
|
+
*/
|
|
60
|
+
async function loadConfiguration(): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
setIsLoading(true);
|
|
63
|
+
setErrorMessage(null);
|
|
64
|
+
const response = await fetch('/api/admin/code-runners', { cache: 'no-store' });
|
|
65
|
+
const payload = (await response.json()) as CodeRunnersResponse;
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(payload.error || 'Failed to load code-runner configuration.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setAgent(payload.agent || 'github-copilot');
|
|
72
|
+
setModel(payload.model || 'gpt-5.4');
|
|
73
|
+
setThinkingLevel(payload.thinkingLevel || 'xhigh');
|
|
74
|
+
setStatus(payload.status || '');
|
|
75
|
+
} catch (error) {
|
|
76
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load code-runner configuration.');
|
|
77
|
+
} finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Saves code-runner settings into `.env`.
|
|
84
|
+
*/
|
|
85
|
+
async function saveConfiguration(applyRuntimeConfiguration: boolean): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
setIsSaving(true);
|
|
88
|
+
setErrorMessage(null);
|
|
89
|
+
setSuccessMessage(null);
|
|
90
|
+
setApplyOutput(null);
|
|
91
|
+
|
|
92
|
+
const response = await fetch('/api/admin/code-runners', {
|
|
93
|
+
method: 'PATCH',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({ agent, model, thinkingLevel, applyRuntimeConfiguration }),
|
|
98
|
+
});
|
|
99
|
+
const payload = (await response.json()) as CodeRunnersResponse;
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(payload.error || 'Failed to save code-runner configuration.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setAgent(payload.agent || agent);
|
|
106
|
+
setModel(payload.model || model);
|
|
107
|
+
setThinkingLevel(payload.thinkingLevel || thinkingLevel);
|
|
108
|
+
setStatus(payload.status || '');
|
|
109
|
+
setApplyOutput(payload.applyResult?.output || null);
|
|
110
|
+
setSuccessMessage(
|
|
111
|
+
applyRuntimeConfiguration
|
|
112
|
+
? 'Code-runner configuration was saved and applied.'
|
|
113
|
+
: 'Code-runner configuration was saved.',
|
|
114
|
+
);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to save code-runner configuration.');
|
|
117
|
+
} finally {
|
|
118
|
+
setIsSaving(false);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="container mx-auto space-y-6 px-4 py-8">
|
|
124
|
+
<div className="mt-20">
|
|
125
|
+
<h1 className="text-3xl font-light text-gray-900">Code runners</h1>
|
|
126
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
127
|
+
Configure the local runner used by the standalone Agents Server durable chat worker.
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{errorMessage && (
|
|
132
|
+
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
133
|
+
{errorMessage}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
{successMessage && (
|
|
137
|
+
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
|
|
138
|
+
{successMessage}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
143
|
+
<div className="grid gap-5 md:grid-cols-3">
|
|
144
|
+
<label className="space-y-2">
|
|
145
|
+
<span className="text-sm font-semibold text-slate-700">Runner</span>
|
|
146
|
+
<select
|
|
147
|
+
value={agent}
|
|
148
|
+
onChange={(event) => setAgent(event.target.value)}
|
|
149
|
+
disabled={isLoading || isSaving}
|
|
150
|
+
className={INPUT_CLASS_NAME}
|
|
151
|
+
>
|
|
152
|
+
{RUNNER_OPTIONS.map((option) => (
|
|
153
|
+
<option key={option.value} value={option.value}>
|
|
154
|
+
{option.label}
|
|
155
|
+
</option>
|
|
156
|
+
))}
|
|
157
|
+
</select>
|
|
158
|
+
</label>
|
|
159
|
+
<label className="space-y-2">
|
|
160
|
+
<span className="text-sm font-semibold text-slate-700">Model</span>
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
value={model}
|
|
164
|
+
onChange={(event) => setModel(event.target.value)}
|
|
165
|
+
disabled={isLoading || isSaving}
|
|
166
|
+
className={INPUT_CLASS_NAME}
|
|
167
|
+
/>
|
|
168
|
+
</label>
|
|
169
|
+
<label className="space-y-2">
|
|
170
|
+
<span className="text-sm font-semibold text-slate-700">Thinking level</span>
|
|
171
|
+
<input
|
|
172
|
+
type="text"
|
|
173
|
+
value={thinkingLevel}
|
|
174
|
+
onChange={(event) => setThinkingLevel(event.target.value)}
|
|
175
|
+
disabled={isLoading || isSaving}
|
|
176
|
+
className={INPUT_CLASS_NAME}
|
|
177
|
+
/>
|
|
178
|
+
</label>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="mt-6 flex flex-wrap items-center gap-3">
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => void saveConfiguration(false)}
|
|
185
|
+
disabled={isLoading || isSaving}
|
|
186
|
+
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
187
|
+
>
|
|
188
|
+
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
189
|
+
Save runner
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => void saveConfiguration(true)}
|
|
194
|
+
disabled={isLoading || isSaving}
|
|
195
|
+
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"
|
|
196
|
+
>
|
|
197
|
+
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ServerCog className="h-4 w-4" />}
|
|
198
|
+
Save and apply runner
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</Card>
|
|
202
|
+
|
|
203
|
+
{applyOutput ? (
|
|
204
|
+
<pre className="max-h-72 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
|
|
205
|
+
{applyOutput}
|
|
206
|
+
</pre>
|
|
207
|
+
) : null}
|
|
208
|
+
|
|
209
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
210
|
+
<div className="space-y-3">
|
|
211
|
+
<h2 className="text-lg font-semibold text-slate-900">Authentication</h2>
|
|
212
|
+
<p className="text-sm text-slate-600">
|
|
213
|
+
GitHub Copilot still requires an interactive CLI login and project trust setup on the VPS
|
|
214
|
+
terminal. Use <span className="font-mono">sudo -u $USER copilot</span>, run{' '}
|
|
215
|
+
<span className="font-mono">/login</span> when prompted, trust the install directory, then
|
|
216
|
+
restart the pm2 process.
|
|
217
|
+
</p>
|
|
218
|
+
<pre className="max-h-64 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
|
|
219
|
+
{status || 'Runner status was not available.'}
|
|
220
|
+
</pre>
|
|
221
|
+
</div>
|
|
222
|
+
</Card>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
+
import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
|
|
3
|
+
import { CodeRunnersClient } from './CodeRunnersClient';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Super-admin page for configuring standalone code runners.
|
|
7
|
+
*/
|
|
8
|
+
export default async function CodeRunnersPage() {
|
|
9
|
+
if (!(await isUserGlobalAdmin())) {
|
|
10
|
+
return <ForbiddenPage />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <CodeRunnersClient />;
|
|
14
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { EyeOff, Loader2, Save, ServerCog } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { Card } from '../../../components/Homepage/Card';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One environment variable returned by the admin API.
|
|
9
|
+
*/
|
|
10
|
+
type EnvironmentVariableRecord = {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
readonly value: string;
|
|
13
|
+
readonly isSensitive: boolean;
|
|
14
|
+
readonly isDefined: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Environment API response consumed by the client page.
|
|
19
|
+
*/
|
|
20
|
+
type EnvironmentVariablesResponse = {
|
|
21
|
+
readonly envFilePath: string;
|
|
22
|
+
readonly variables: ReadonlyArray<EnvironmentVariableRecord>;
|
|
23
|
+
readonly canEdit: boolean;
|
|
24
|
+
readonly error?: string;
|
|
25
|
+
readonly applyResult?: {
|
|
26
|
+
readonly isAvailable: boolean;
|
|
27
|
+
readonly output: string;
|
|
28
|
+
} | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared input styling for environment rows.
|
|
33
|
+
*/
|
|
34
|
+
const INPUT_CLASS_NAME =
|
|
35
|
+
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm font-mono text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:bg-gray-50 disabled:text-gray-500';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Placeholder shown instead of sensitive environment values.
|
|
39
|
+
*/
|
|
40
|
+
const HIDDEN_ENVIRONMENT_VALUE = '********';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Browser UI for standalone VPS environment variables.
|
|
44
|
+
*/
|
|
45
|
+
export function EnvironmentVariablesClient() {
|
|
46
|
+
const [envFilePath, setEnvFilePath] = useState('');
|
|
47
|
+
const [variables, setVariables] = useState<EnvironmentVariableRecord[]>([]);
|
|
48
|
+
const [draftValues, setDraftValues] = useState<Record<string, string>>({});
|
|
49
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
50
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
51
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
52
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
53
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
54
|
+
const [applyOutput, setApplyOutput] = useState<string | null>(null);
|
|
55
|
+
|
|
56
|
+
const hasChanges = useMemo(
|
|
57
|
+
() => variables.some((variable) => draftValues[variable.key] !== variable.value),
|
|
58
|
+
[draftValues, variables],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
void loadVariables();
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Loads environment variables from the admin API.
|
|
67
|
+
*/
|
|
68
|
+
async function loadVariables(): Promise<void> {
|
|
69
|
+
try {
|
|
70
|
+
setIsLoading(true);
|
|
71
|
+
setErrorMessage(null);
|
|
72
|
+
const response = await fetch('/api/admin/environment', { cache: 'no-store' });
|
|
73
|
+
const payload = (await response.json()) as EnvironmentVariablesResponse;
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(payload.error || 'Failed to load environment variables.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setEnvFilePath(payload.envFilePath);
|
|
80
|
+
setVariables([...payload.variables]);
|
|
81
|
+
setDraftValues(Object.fromEntries(payload.variables.map((variable) => [variable.key, variable.value])));
|
|
82
|
+
setCanEdit(payload.canEdit);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load environment variables.');
|
|
85
|
+
} finally {
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Persists current environment drafts.
|
|
92
|
+
*
|
|
93
|
+
* @param applyRuntimeConfiguration - Whether to run the VPS apply step after saving.
|
|
94
|
+
*/
|
|
95
|
+
async function saveVariables(applyRuntimeConfiguration: boolean): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
setIsSaving(true);
|
|
98
|
+
setErrorMessage(null);
|
|
99
|
+
setSuccessMessage(null);
|
|
100
|
+
setApplyOutput(null);
|
|
101
|
+
|
|
102
|
+
const response = await fetch('/api/admin/environment', {
|
|
103
|
+
method: 'PATCH',
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
variables: draftValues,
|
|
109
|
+
applyRuntimeConfiguration,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
const payload = (await response.json()) as EnvironmentVariablesResponse;
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(payload.error || 'Failed to save environment variables.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setEnvFilePath(payload.envFilePath);
|
|
119
|
+
setVariables([...payload.variables]);
|
|
120
|
+
setDraftValues(Object.fromEntries(payload.variables.map((variable) => [variable.key, variable.value])));
|
|
121
|
+
setCanEdit(payload.canEdit);
|
|
122
|
+
setSuccessMessage('Environment variables were saved.');
|
|
123
|
+
setApplyOutput(payload.applyResult?.output || null);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to save environment variables.');
|
|
126
|
+
} finally {
|
|
127
|
+
setIsSaving(false);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-6">
|
|
133
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
134
|
+
<div className="space-y-2 text-sm text-slate-600">
|
|
135
|
+
<p>
|
|
136
|
+
These values are stored in the VPS-wide <span className="font-mono">.env</span> file and affect
|
|
137
|
+
the whole installed Agents Server process.
|
|
138
|
+
</p>
|
|
139
|
+
<p>
|
|
140
|
+
Sensitive values are always masked. To change a secret, type a new value; leaving the stars in
|
|
141
|
+
place keeps the current secret.
|
|
142
|
+
</p>
|
|
143
|
+
{envFilePath ? <p className="font-mono text-xs text-slate-500">{envFilePath}</p> : null}
|
|
144
|
+
</div>
|
|
145
|
+
</Card>
|
|
146
|
+
|
|
147
|
+
{!canEdit && (
|
|
148
|
+
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
|
149
|
+
You can view environment variables as an administrator. Editing is restricted to the super admin
|
|
150
|
+
authenticated with <span className="font-mono">ADMIN_PASSWORD</span>.
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{errorMessage && (
|
|
155
|
+
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
156
|
+
{errorMessage}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
{successMessage && (
|
|
160
|
+
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
|
|
161
|
+
{successMessage}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
{applyOutput && (
|
|
165
|
+
<pre className="max-h-72 overflow-auto rounded-xl border border-slate-200 bg-slate-950 p-4 text-xs text-slate-100">
|
|
166
|
+
{applyOutput}
|
|
167
|
+
</pre>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<Card className="hover:border-gray-200 hover:shadow-md">
|
|
171
|
+
{isLoading ? (
|
|
172
|
+
<div className="py-10 text-center text-sm text-gray-500">Loading environment variables...</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="overflow-x-auto rounded-xl border border-gray-200">
|
|
175
|
+
<table className="min-w-full table-fixed divide-y divide-gray-200 text-sm">
|
|
176
|
+
<colgroup>
|
|
177
|
+
<col className="w-[18rem]" />
|
|
178
|
+
<col />
|
|
179
|
+
<col className="w-[10rem]" />
|
|
180
|
+
</colgroup>
|
|
181
|
+
<thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-500">
|
|
182
|
+
<tr>
|
|
183
|
+
<th className="px-4 py-3 text-left font-semibold">Variable</th>
|
|
184
|
+
<th className="px-4 py-3 text-left font-semibold">Value</th>
|
|
185
|
+
<th className="px-4 py-3 text-left font-semibold">Status</th>
|
|
186
|
+
</tr>
|
|
187
|
+
</thead>
|
|
188
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
189
|
+
{variables.map((variable) => (
|
|
190
|
+
<tr key={variable.key}>
|
|
191
|
+
<td className="px-4 py-3 align-top font-mono text-sm font-semibold text-slate-800">
|
|
192
|
+
{variable.key}
|
|
193
|
+
</td>
|
|
194
|
+
<td className="px-4 py-3 align-top">
|
|
195
|
+
<div className="relative">
|
|
196
|
+
<input
|
|
197
|
+
type={variable.isSensitive ? 'password' : 'text'}
|
|
198
|
+
value={draftValues[variable.key] ?? ''}
|
|
199
|
+
onChange={(event) =>
|
|
200
|
+
setDraftValues((currentDraftValues) => ({
|
|
201
|
+
...currentDraftValues,
|
|
202
|
+
[variable.key]: event.target.value,
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
disabled={!canEdit || isSaving}
|
|
206
|
+
className={`${INPUT_CLASS_NAME} ${
|
|
207
|
+
variable.isSensitive ? 'pr-10 tracking-wider' : ''
|
|
208
|
+
}`}
|
|
209
|
+
placeholder={variable.isSensitive ? HIDDEN_ENVIRONMENT_VALUE : ''}
|
|
210
|
+
/>
|
|
211
|
+
{variable.isSensitive && (
|
|
212
|
+
<EyeOff className="pointer-events-none absolute right-3 top-2.5 h-4 w-4 text-slate-400" />
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</td>
|
|
216
|
+
<td className="px-4 py-3 align-top">
|
|
217
|
+
<span
|
|
218
|
+
className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${
|
|
219
|
+
variable.isDefined
|
|
220
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
221
|
+
: 'border-slate-200 bg-slate-50 text-slate-500'
|
|
222
|
+
}`}
|
|
223
|
+
>
|
|
224
|
+
{variable.isDefined ? 'Configured' : 'Empty'}
|
|
225
|
+
</span>
|
|
226
|
+
</td>
|
|
227
|
+
</tr>
|
|
228
|
+
))}
|
|
229
|
+
</tbody>
|
|
230
|
+
</table>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{canEdit && (
|
|
236
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={() => void saveVariables(false)}
|
|
240
|
+
disabled={isSaving || !hasChanges}
|
|
241
|
+
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
242
|
+
>
|
|
243
|
+
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
244
|
+
Save .env
|
|
245
|
+
</button>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => void saveVariables(true)}
|
|
249
|
+
disabled={isSaving}
|
|
250
|
+
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"
|
|
251
|
+
>
|
|
252
|
+
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ServerCog className="h-4 w-4" />}
|
|
253
|
+
Save and apply VPS configuration
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
3
|
+
import { AdminConfigurationShell } from '../_components/AdminConfigurationShell';
|
|
4
|
+
import { EnvironmentVariablesClient } from './EnvironmentVariablesClient';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Page for viewing and editing standalone VPS `.env` variables.
|
|
8
|
+
*/
|
|
9
|
+
export default async function EnvironmentVariablesPage() {
|
|
10
|
+
const isAdmin = await isUserAdmin();
|
|
11
|
+
|
|
12
|
+
if (!isAdmin) {
|
|
13
|
+
return <ForbiddenPage />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<AdminConfigurationShell activePage="environment">
|
|
18
|
+
<EnvironmentVariablesClient />
|
|
19
|
+
</AdminConfigurationShell>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, RefreshCcw } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* API payload returned by the pm2 logs endpoint.
|
|
8
|
+
*/
|
|
9
|
+
type LogsResponse = {
|
|
10
|
+
readonly isAvailable?: boolean;
|
|
11
|
+
readonly output?: string;
|
|
12
|
+
readonly error?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Client UI for recent pm2 log output.
|
|
17
|
+
*/
|
|
18
|
+
export function LogsClient() {
|
|
19
|
+
const [logs, setLogs] = useState('');
|
|
20
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
21
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
void loadLogs();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Refreshes log output from the API.
|
|
29
|
+
*/
|
|
30
|
+
async function loadLogs(): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
setErrorMessage(null);
|
|
34
|
+
const response = await fetch('/api/admin/logs?lines=300', { cache: 'no-store' });
|
|
35
|
+
const payload = (await response.json()) as LogsResponse;
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(payload.error || 'Failed to load logs.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setLogs(payload.output || 'No log output returned.');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load logs.');
|
|
44
|
+
} finally {
|
|
45
|
+
setIsLoading(false);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="container mx-auto space-y-6 px-4 py-8">
|
|
51
|
+
<div className="mt-20 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
52
|
+
<div>
|
|
53
|
+
<h1 className="text-3xl font-light text-gray-900">Logs</h1>
|
|
54
|
+
<p className="mt-1 text-sm text-gray-500">Recent pm2 output for the standalone Agents Server.</p>
|
|
55
|
+
</div>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => void loadLogs()}
|
|
59
|
+
disabled={isLoading}
|
|
60
|
+
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"
|
|
61
|
+
>
|
|
62
|
+
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCcw className="h-4 w-4" />}
|
|
63
|
+
Refresh
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{errorMessage && (
|
|
68
|
+
<div className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
69
|
+
{errorMessage}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<pre className="min-h-[28rem] overflow-auto rounded-xl border border-slate-800 bg-slate-950 p-4 text-xs leading-5 text-slate-100 shadow-sm">
|
|
74
|
+
{isLoading && !logs ? 'Loading logs...' : logs}
|
|
75
|
+
</pre>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
+
import { isUserGlobalAdmin } from '../../../utils/isUserGlobalAdmin';
|
|
3
|
+
import { LogsClient } from './LogsClient';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Super-admin page for viewing standalone VPS pm2 logs.
|
|
7
|
+
*/
|
|
8
|
+
export default async function LogsPage() {
|
|
9
|
+
if (!(await isUserGlobalAdmin())) {
|
|
10
|
+
return <ForbiddenPage />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <LogsClient />;
|
|
14
|
+
}
|