@promptbook/cli 0.112.0-93 → 0.112.0-96
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/next.config.ts +8 -1
- package/apps/agents-server/playwright.config.ts +2 -0
- 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/database/DatabaseAdminClient.tsx +38 -0
- package/apps/agents-server/src/app/admin/database/DatabaseAdminStudioSurface.tsx +42 -0
- package/apps/agents-server/src/app/admin/database/page.tsx +33 -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/database/studio/route.ts +113 -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/layout.tsx +1 -0
- package/apps/agents-server/src/app/page.tsx +101 -1
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +27 -4
- package/apps/agents-server/src/database/$provideClientSql.ts +2 -6
- package/apps/agents-server/src/database/$provideDatabaseAdminExecutor.ts +273 -0
- package/apps/agents-server/src/database/resolvePostgresConnectionString.ts +26 -0
- package/apps/agents-server/src/database/sqlite/$provideAgentsServerSqliteDatabase.ts +83 -0
- package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +20 -71
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +5 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +5 -0
- package/apps/agents-server/src/languages/translations/english.yaml +5 -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 +3 -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 +712 -829
- 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
|
@@ -138,6 +138,11 @@ type DeleteCurrentServerConfirmationResult = {
|
|
|
138
138
|
* @private function of <ServersClient/>
|
|
139
139
|
*/
|
|
140
140
|
type UseServersRegistryStateResult = {
|
|
141
|
+
/**
|
|
142
|
+
* Whether the current viewer can mutate server rows.
|
|
143
|
+
*/
|
|
144
|
+
readonly canEdit: boolean;
|
|
145
|
+
|
|
141
146
|
/**
|
|
142
147
|
* Server resolved from the current request domain.
|
|
143
148
|
*/
|
|
@@ -429,12 +434,13 @@ function useServersRegistryDraftState(servers: ReadonlyArray<ManagedServerRow>)
|
|
|
429
434
|
*/
|
|
430
435
|
function useServersRegistryReloadAction(options: {
|
|
431
436
|
readonly replaceServerDrafts: (servers: ReadonlyArray<ManagedServerRow>) => void;
|
|
437
|
+
readonly setCanEdit: (canEdit: boolean) => void;
|
|
432
438
|
readonly setCurrentServerId: (currentServerId: number | null) => void;
|
|
433
439
|
readonly setError: (error: string | null) => void;
|
|
434
440
|
readonly setLoading: (isLoading: boolean) => void;
|
|
435
441
|
readonly setServers: (servers: ManagedServerRow[]) => void;
|
|
436
442
|
}) {
|
|
437
|
-
const { replaceServerDrafts, setCurrentServerId, setError, setLoading, setServers } = options;
|
|
443
|
+
const { replaceServerDrafts, setCanEdit, setCurrentServerId, setError, setLoading, setServers } = options;
|
|
438
444
|
|
|
439
445
|
return useCallback(async () => {
|
|
440
446
|
setLoading(true);
|
|
@@ -444,13 +450,14 @@ function useServersRegistryReloadAction(options: {
|
|
|
444
450
|
const payload = await ServersRegistryApi.fetchServers();
|
|
445
451
|
setServers([...payload.servers]);
|
|
446
452
|
setCurrentServerId(payload.currentServerId);
|
|
453
|
+
setCanEdit(payload.canEdit);
|
|
447
454
|
replaceServerDrafts(payload.servers);
|
|
448
455
|
} catch (loadError) {
|
|
449
456
|
setError(resolveServersRegistryActionErrorMessage(loadError, 'Failed to load servers.'));
|
|
450
457
|
} finally {
|
|
451
458
|
setLoading(false);
|
|
452
459
|
}
|
|
453
|
-
}, [replaceServerDrafts, setCurrentServerId, setError, setLoading, setServers]);
|
|
460
|
+
}, [replaceServerDrafts, setCanEdit, setCurrentServerId, setError, setLoading, setServers]);
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
/**
|
|
@@ -618,6 +625,7 @@ function useDeleteCurrentServerAction(options: {
|
|
|
618
625
|
*/
|
|
619
626
|
export function useServersRegistryState(): UseServersRegistryStateResult {
|
|
620
627
|
const [servers, setServers] = useState<ManagedServerRow[]>([]);
|
|
628
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
621
629
|
const [loading, setLoading] = useState(true);
|
|
622
630
|
const [error, setError] = useState<string | null>(null);
|
|
623
631
|
const [currentServerId, setCurrentServerId] = useState<number | null>(null);
|
|
@@ -635,6 +643,7 @@ export function useServersRegistryState(): UseServersRegistryStateResult {
|
|
|
635
643
|
|
|
636
644
|
const reloadServers = useServersRegistryReloadAction({
|
|
637
645
|
replaceServerDrafts,
|
|
646
|
+
setCanEdit,
|
|
638
647
|
setCurrentServerId,
|
|
639
648
|
setError,
|
|
640
649
|
setLoading,
|
|
@@ -671,6 +680,7 @@ export function useServersRegistryState(): UseServersRegistryStateResult {
|
|
|
671
680
|
});
|
|
672
681
|
|
|
673
682
|
return {
|
|
683
|
+
canEdit,
|
|
674
684
|
currentServer,
|
|
675
685
|
currentServerId,
|
|
676
686
|
deletingServerId,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
5
|
+
import {
|
|
6
|
+
applyVpsCodeRunnerConfiguration,
|
|
7
|
+
listVpsEnvironmentVariables,
|
|
8
|
+
updateVpsEnvironmentVariables,
|
|
9
|
+
} from '@/src/utils/vpsConfiguration';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loads configured code-runner settings from the editable VPS environment.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET() {
|
|
17
|
+
if (!(await isUserGlobalAdmin())) {
|
|
18
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const snapshot = await listVpsEnvironmentVariables();
|
|
23
|
+
const environmentByKey = Object.fromEntries(
|
|
24
|
+
snapshot.variables.map((variable) => [variable.key, variable.value]),
|
|
25
|
+
) as Record<string, string>;
|
|
26
|
+
|
|
27
|
+
return NextResponse.json({
|
|
28
|
+
agent: environmentByKey.PTBK_AGENT || process.env.PTBK_AGENT || 'github-copilot',
|
|
29
|
+
model: environmentByKey.PTBK_MODEL || process.env.PTBK_MODEL || 'gpt-5.4',
|
|
30
|
+
thinkingLevel: environmentByKey.PTBK_THINKING_LEVEL || process.env.PTBK_THINKING_LEVEL || 'xhigh',
|
|
31
|
+
status: await resolveRunnerStatus(environmentByKey.PTBK_AGENT || process.env.PTBK_AGENT || 'github-copilot'),
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: error instanceof Error ? error.message : 'Failed to load code-runner configuration.' },
|
|
36
|
+
{ status: 500 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Updates code-runner environment variables.
|
|
43
|
+
*/
|
|
44
|
+
export async function PATCH(request: Request) {
|
|
45
|
+
if (!(await isUserGlobalAdmin())) {
|
|
46
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const body = (await request.json().catch(() => null)) as
|
|
51
|
+
| {
|
|
52
|
+
readonly agent?: string;
|
|
53
|
+
readonly model?: string;
|
|
54
|
+
readonly thinkingLevel?: string;
|
|
55
|
+
readonly applyRuntimeConfiguration?: boolean;
|
|
56
|
+
}
|
|
57
|
+
| null;
|
|
58
|
+
|
|
59
|
+
if (!body) {
|
|
60
|
+
return NextResponse.json({ error: 'Code-runner payload is required.' }, { status: 400 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await updateVpsEnvironmentVariables({
|
|
64
|
+
PTBK_AGENT: body.agent || '',
|
|
65
|
+
PTBK_MODEL: body.model || '',
|
|
66
|
+
PTBK_THINKING_LEVEL: body.thinkingLevel || '',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const response = await GET();
|
|
70
|
+
const payload = (await response.json()) as Record<string, unknown>;
|
|
71
|
+
|
|
72
|
+
return NextResponse.json({
|
|
73
|
+
...payload,
|
|
74
|
+
applyResult: body.applyRuntimeConfiguration ? await applyVpsCodeRunnerConfiguration() : null,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return NextResponse.json(
|
|
78
|
+
{ error: error instanceof Error ? error.message : 'Failed to update code-runner configuration.' },
|
|
79
|
+
{ status: 500 },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolves a short runner-authentication status for the configured runner.
|
|
86
|
+
*
|
|
87
|
+
* @param agent - Runner id.
|
|
88
|
+
* @returns Human-readable status.
|
|
89
|
+
*/
|
|
90
|
+
async function resolveRunnerStatus(agent: string): Promise<string> {
|
|
91
|
+
if (agent !== 'github-copilot') {
|
|
92
|
+
return 'Status check is currently available for GitHub Copilot CLI only.';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const { stdout, stderr } = await execFileAsync('copilot', ['auth', 'status'], {
|
|
97
|
+
timeout: 10_000,
|
|
98
|
+
maxBuffer: 128 * 1024,
|
|
99
|
+
});
|
|
100
|
+
return [stdout, stderr].filter(Boolean).join('\n').trim() || 'GitHub Copilot CLI returned no status output.';
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return error instanceof Error ? error.message : 'GitHub Copilot CLI status check failed.';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { serializeError, type StudioBFFRequest } from '@prisma/studio-core/data/bff';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import {
|
|
4
|
+
$provideDatabaseAdminExecutor,
|
|
5
|
+
type DatabaseAdminExecutor,
|
|
6
|
+
type DatabaseAdminQuery,
|
|
7
|
+
} from '../../../../../database/$provideDatabaseAdminExecutor';
|
|
8
|
+
import { isUserGlobalAdmin } from '../../../../../utils/isUserGlobalAdmin';
|
|
9
|
+
|
|
10
|
+
export const runtime = 'nodejs';
|
|
11
|
+
export const dynamic = 'force-dynamic';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handles Embedded Prisma Studio SQL requests for the configured Agents Server database.
|
|
15
|
+
*
|
|
16
|
+
* @param request - Incoming Studio BFF request.
|
|
17
|
+
* @returns Studio-compatible JSON response.
|
|
18
|
+
*/
|
|
19
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
20
|
+
if (!(await isUserGlobalAdmin())) {
|
|
21
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let payload: StudioBFFRequest;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
payload = (await request.json()) as StudioBFFRequest;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return NextResponse.json([serializeError(error)], { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return handleDatabaseAdminStudioRequest($provideDatabaseAdminExecutor(), payload);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves one Studio BFF payload against the raw database executor.
|
|
37
|
+
*
|
|
38
|
+
* @param executor - Raw SQL executor for the active database backend.
|
|
39
|
+
* @param payload - Studio BFF request payload.
|
|
40
|
+
* @returns Studio-compatible JSON response.
|
|
41
|
+
*/
|
|
42
|
+
async function handleDatabaseAdminStudioRequest(
|
|
43
|
+
executor: DatabaseAdminExecutor,
|
|
44
|
+
payload: StudioBFFRequest,
|
|
45
|
+
): Promise<NextResponse> {
|
|
46
|
+
try {
|
|
47
|
+
switch (payload.procedure) {
|
|
48
|
+
case 'query':
|
|
49
|
+
return NextResponse.json([null, await executor.execute(payload.query as DatabaseAdminQuery)]);
|
|
50
|
+
|
|
51
|
+
case 'sequence':
|
|
52
|
+
return handleDatabaseAdminStudioSequence(executor, payload.sequence as readonly [
|
|
53
|
+
DatabaseAdminQuery,
|
|
54
|
+
DatabaseAdminQuery,
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
case 'transaction':
|
|
58
|
+
return NextResponse.json([
|
|
59
|
+
null,
|
|
60
|
+
await executor.executeTransaction(payload.queries as ReadonlyArray<DatabaseAdminQuery>),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
case 'sql-lint':
|
|
64
|
+
return NextResponse.json([
|
|
65
|
+
null,
|
|
66
|
+
await executor.lintSql({
|
|
67
|
+
schemaVersion: payload.schemaVersion,
|
|
68
|
+
sql: payload.sql,
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
default:
|
|
73
|
+
return NextResponse.json([serializeError(new Error('Invalid database admin procedure.'))], {
|
|
74
|
+
status: 400,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return NextResponse.json([serializeError(error)]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Executes a two-step Studio sequence, preserving the response shape expected by the BFF client.
|
|
84
|
+
*
|
|
85
|
+
* @param executor - Raw SQL executor for the active database backend.
|
|
86
|
+
* @param sequence - Exactly two Studio-generated queries.
|
|
87
|
+
* @returns Studio-compatible sequence response.
|
|
88
|
+
*/
|
|
89
|
+
async function handleDatabaseAdminStudioSequence(
|
|
90
|
+
executor: DatabaseAdminExecutor,
|
|
91
|
+
sequence: readonly [DatabaseAdminQuery, DatabaseAdminQuery],
|
|
92
|
+
): Promise<NextResponse> {
|
|
93
|
+
const [firstQuery, secondQuery] = sequence;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const firstResult = await executor.execute(firstQuery);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const secondResult = await executor.execute(secondQuery);
|
|
100
|
+
return NextResponse.json([
|
|
101
|
+
[null, firstResult],
|
|
102
|
+
[null, secondResult],
|
|
103
|
+
]);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return NextResponse.json([
|
|
106
|
+
[null, firstResult],
|
|
107
|
+
[serializeError(error)],
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return NextResponse.json([[serializeError(error)]]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
applyVpsRuntimeConfiguration,
|
|
4
|
+
listVpsEnvironmentVariables,
|
|
5
|
+
updateVpsEnvironmentVariables,
|
|
6
|
+
} from '@/src/utils/vpsConfiguration';
|
|
7
|
+
import { isUserAdmin } from '@/src/utils/isUserAdmin';
|
|
8
|
+
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loads `.env` variables visible in the admin UI.
|
|
12
|
+
*/
|
|
13
|
+
export async function GET() {
|
|
14
|
+
if (!(await isUserAdmin())) {
|
|
15
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
...(await listVpsEnvironmentVariables()),
|
|
21
|
+
canEdit: await isUserGlobalAdmin(),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: error instanceof Error ? error.message : 'Failed to load environment variables.' },
|
|
26
|
+
{ status: 500 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Persists editable `.env` variables and optionally reapplies VPS runtime configuration.
|
|
33
|
+
*/
|
|
34
|
+
export async function PATCH(request: Request) {
|
|
35
|
+
if (!(await isUserGlobalAdmin())) {
|
|
36
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const body = (await request.json().catch(() => null)) as
|
|
41
|
+
| {
|
|
42
|
+
readonly variables?: Record<string, string>;
|
|
43
|
+
readonly applyRuntimeConfiguration?: boolean;
|
|
44
|
+
}
|
|
45
|
+
| null;
|
|
46
|
+
|
|
47
|
+
if (!body || typeof body.variables !== 'object' || body.variables === null) {
|
|
48
|
+
return NextResponse.json({ error: 'Variables payload is required.' }, { status: 400 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const snapshot = await updateVpsEnvironmentVariables(body.variables);
|
|
52
|
+
const applyResult = body.applyRuntimeConfiguration ? await applyVpsRuntimeConfiguration() : null;
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
...snapshot,
|
|
56
|
+
canEdit: true,
|
|
57
|
+
applyResult,
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{ error: error instanceof Error ? error.message : 'Failed to update environment variables.' },
|
|
62
|
+
{ status: 500 },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
|
|
3
|
+
import { readVpsPm2Logs } from '@/src/utils/vpsConfiguration';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads recent pm2 logs for the standalone Agents Server process.
|
|
7
|
+
*/
|
|
8
|
+
export async function GET(request: Request) {
|
|
9
|
+
if (!(await isUserGlobalAdmin())) {
|
|
10
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
const rawLines = Number.parseInt(url.searchParams.get('lines') || '200', 10);
|
|
16
|
+
const lineCount = Number.isFinite(rawLines) ? Math.min(Math.max(rawLines, 20), 1000) : 200;
|
|
17
|
+
return NextResponse.json(await readVpsPm2Logs(lineCount));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: error instanceof Error ? error.message : 'Failed to load pm2 logs.' },
|
|
21
|
+
{ status: 500 },
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isAgentsServerSqliteMode } from '../../../../../database/agentsServerDatabaseMode';
|
|
2
3
|
import { resolveCurrentServerRegistryContext } from '../../../../../utils/currentServerRegistryContext';
|
|
3
4
|
import { isUserGlobalAdmin } from '../../../../../utils/isUserGlobalAdmin';
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createServerPublicUrl,
|
|
7
|
+
listEnvironmentRegisteredServers,
|
|
8
|
+
normalizeServerDomain,
|
|
9
|
+
} from '../../../../../utils/serverRegistry';
|
|
5
10
|
import {
|
|
6
11
|
assertGlobalAdminAccess,
|
|
7
12
|
deleteManagedServer,
|
|
@@ -10,6 +15,11 @@ import {
|
|
|
10
15
|
updateManagedServer,
|
|
11
16
|
type UpdateServerInput,
|
|
12
17
|
} from '../../../../../utils/serverManagement';
|
|
18
|
+
import {
|
|
19
|
+
applyVpsRuntimeConfiguration,
|
|
20
|
+
listConfiguredVpsDomains,
|
|
21
|
+
updateConfiguredVpsDomains,
|
|
22
|
+
} from '../../../../../utils/vpsConfiguration';
|
|
13
23
|
|
|
14
24
|
/**
|
|
15
25
|
* Updates editable `_Server` fields for one registered server.
|
|
@@ -24,8 +34,15 @@ export async function PATCH(request: Request, context: { params: Promise<{ serve
|
|
|
24
34
|
|
|
25
35
|
const { serverId } = await context.params;
|
|
26
36
|
const body = (await request.json()) as Omit<UpdateServerInput, 'id'>;
|
|
37
|
+
const parsedServerId = parseManagedServerId(serverId);
|
|
38
|
+
|
|
39
|
+
if (isAgentsServerSqliteMode()) {
|
|
40
|
+
const updatedServer = await updateStandaloneVpsServerDomain(parsedServerId, body.domain);
|
|
41
|
+
return NextResponse.json({ server: updatedServer });
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
const updatedServer = await updateManagedServer({
|
|
28
|
-
id:
|
|
45
|
+
id: parsedServerId,
|
|
29
46
|
...body,
|
|
30
47
|
});
|
|
31
48
|
|
|
@@ -52,9 +69,19 @@ export async function DELETE(_request: Request, context: { params: Promise<{ ser
|
|
|
52
69
|
assertGlobalAdminAccess(await isUserGlobalAdmin());
|
|
53
70
|
|
|
54
71
|
const { serverId } = await context.params;
|
|
72
|
+
const parsedServerId = parseManagedServerId(serverId);
|
|
73
|
+
|
|
74
|
+
if (isAgentsServerSqliteMode()) {
|
|
75
|
+
await deleteStandaloneVpsServerDomain(parsedServerId);
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
success: true,
|
|
78
|
+
redirectUrl: null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
const currentContext = await resolveCurrentServerRegistryContext();
|
|
56
83
|
const nextServerId = await deleteManagedServer({
|
|
57
|
-
serverId:
|
|
84
|
+
serverId: parsedServerId,
|
|
58
85
|
currentServerId: currentContext.currentServer?.id ?? null,
|
|
59
86
|
});
|
|
60
87
|
const nextServer =
|
|
@@ -76,3 +103,52 @@ export async function DELETE(_request: Request, context: { params: Promise<{ ser
|
|
|
76
103
|
);
|
|
77
104
|
}
|
|
78
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Updates a virtual standalone VPS server by replacing its domain in `SERVERS`.
|
|
109
|
+
*
|
|
110
|
+
* @param serverId - Virtual server id.
|
|
111
|
+
* @param rawDomain - Replacement domain.
|
|
112
|
+
* @returns Updated virtual server row.
|
|
113
|
+
*/
|
|
114
|
+
async function updateStandaloneVpsServerDomain(serverId: number, rawDomain: string) {
|
|
115
|
+
const normalizedDomain = normalizeServerDomain(rawDomain);
|
|
116
|
+
if (!normalizedDomain) {
|
|
117
|
+
throw new Error('A valid domain is required.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const servers = listEnvironmentRegisteredServers();
|
|
121
|
+
const serverIndex = servers.findIndex((server) => server.id === serverId);
|
|
122
|
+
if (serverIndex === -1) {
|
|
123
|
+
throw new Error(`Standalone VPS server ${serverId} was not found.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const domains = await listConfiguredVpsDomains();
|
|
127
|
+
const nextDomains = domains.map((domain, index) => (index === serverIndex ? normalizedDomain : domain));
|
|
128
|
+
await updateConfiguredVpsDomains(nextDomains);
|
|
129
|
+
await applyVpsRuntimeConfiguration();
|
|
130
|
+
|
|
131
|
+
const updatedServer = listEnvironmentRegisteredServers().find((server) => server.domain === normalizedDomain);
|
|
132
|
+
if (!updatedServer) {
|
|
133
|
+
throw new Error(`Standalone VPS server ${normalizedDomain} was not persisted.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return updatedServer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Deletes a virtual standalone VPS server by removing its domain from `SERVERS`.
|
|
141
|
+
*
|
|
142
|
+
* @param serverId - Virtual server id.
|
|
143
|
+
*/
|
|
144
|
+
async function deleteStandaloneVpsServerDomain(serverId: number): Promise<void> {
|
|
145
|
+
const servers = listEnvironmentRegisteredServers();
|
|
146
|
+
const serverIndex = servers.findIndex((server) => server.id === serverId);
|
|
147
|
+
if (serverIndex === -1) {
|
|
148
|
+
throw new Error(`Standalone VPS server ${serverId} was not found.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const domains = await listConfiguredVpsDomains();
|
|
152
|
+
await updateConfiguredVpsDomains(domains.filter((_domain, index) => index !== serverIndex));
|
|
153
|
+
await applyVpsRuntimeConfiguration();
|
|
154
|
+
}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isAgentsServerSqliteMode } from '../../../../database/agentsServerDatabaseMode';
|
|
2
3
|
import { resolveCurrentServerRegistryContext } from '../../../../utils/currentServerRegistryContext';
|
|
4
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
3
5
|
import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
|
|
6
|
+
import {
|
|
7
|
+
createServerPublicUrl,
|
|
8
|
+
listEnvironmentRegisteredServers,
|
|
9
|
+
normalizeServerDomain,
|
|
10
|
+
} from '../../../../utils/serverRegistry';
|
|
4
11
|
import {
|
|
5
12
|
assertGlobalAdminAccess,
|
|
6
13
|
createManagedServer,
|
|
7
14
|
resolveManagedServerErrorStatus,
|
|
8
15
|
type CreateServerInput,
|
|
9
16
|
} from '../../../../utils/serverManagement';
|
|
17
|
+
import {
|
|
18
|
+
applyVpsRuntimeConfiguration,
|
|
19
|
+
listConfiguredVpsDomains,
|
|
20
|
+
updateConfiguredVpsDomains,
|
|
21
|
+
} from '../../../../utils/vpsConfiguration';
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Lists all registered servers together with the server resolved from the current domain.
|
|
@@ -15,12 +27,15 @@ import {
|
|
|
15
27
|
*/
|
|
16
28
|
export async function GET() {
|
|
17
29
|
try {
|
|
18
|
-
|
|
30
|
+
if (!(await isUserAdmin())) {
|
|
31
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
32
|
+
}
|
|
19
33
|
|
|
20
34
|
const context = await resolveCurrentServerRegistryContext();
|
|
21
35
|
return NextResponse.json({
|
|
22
36
|
servers: context.registeredServers,
|
|
23
37
|
currentServerId: context.currentServer?.id ?? null,
|
|
38
|
+
canEdit: await isUserGlobalAdmin(),
|
|
24
39
|
});
|
|
25
40
|
} catch (error) {
|
|
26
41
|
return NextResponse.json(
|
|
@@ -43,6 +58,26 @@ export async function POST(request: Request) {
|
|
|
43
58
|
assertGlobalAdminAccess(await isUserGlobalAdmin());
|
|
44
59
|
|
|
45
60
|
const body = (await request.json()) as CreateServerInput;
|
|
61
|
+
if (isAgentsServerSqliteMode()) {
|
|
62
|
+
const normalizedDomain = normalizeServerDomain(body.domain);
|
|
63
|
+
if (!normalizedDomain) {
|
|
64
|
+
return NextResponse.json({ error: 'A valid domain is required.' }, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const existingDomains = await listConfiguredVpsDomains();
|
|
68
|
+
await updateConfiguredVpsDomains([...existingDomains, normalizedDomain]);
|
|
69
|
+
await applyVpsRuntimeConfiguration();
|
|
70
|
+
const createdServer = listEnvironmentRegisteredServers().find((server) => server.domain === normalizedDomain);
|
|
71
|
+
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{
|
|
74
|
+
server: createdServer ?? null,
|
|
75
|
+
publicUrl: createServerPublicUrl(normalizedDomain).href,
|
|
76
|
+
},
|
|
77
|
+
{ status: 201 },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
const result = await createManagedServer(body);
|
|
47
82
|
|
|
48
83
|
if (!result.ok) {
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { headers } from 'next/headers';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { redirect } from 'next/navigation';
|
|
2
4
|
import { $sideEffect } from '../../../../src/utils/organization/$sideEffect';
|
|
5
|
+
import { ForbiddenPage } from '../components/ForbiddenPage/ForbiddenPage';
|
|
3
6
|
import { HomepagePrimarySections } from '../components/Homepage/HomepagePrimarySections';
|
|
4
7
|
import { $provideServer } from '../tools/$provideServer';
|
|
5
8
|
import { isUserAdmin } from '../utils/isUserAdmin';
|
|
9
|
+
import { isUserGlobalAdmin } from '../utils/isUserGlobalAdmin';
|
|
10
|
+
import {
|
|
11
|
+
createServerPublicUrl,
|
|
12
|
+
listRegisteredServersUsingServiceRole,
|
|
13
|
+
resolveRegisteredServerByHost,
|
|
14
|
+
type ServerRecord,
|
|
15
|
+
} from '../utils/serverRegistry';
|
|
6
16
|
import { getHomePageAgents } from './_data/getHomePageAgents';
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* Renders the simplified agents home page with local and federated agents.
|
|
10
20
|
*/
|
|
11
21
|
export default async function HomePage() {
|
|
12
|
-
|
|
22
|
+
const headerStore = await headers();
|
|
23
|
+
$sideEffect(/* Note: [??] This will ensure dynamic rendering of page and avoid Next.js pre-render */ headerStore);
|
|
24
|
+
|
|
25
|
+
const ipAddressRouting = await resolveIpAddressRouting(headerStore.get('host'));
|
|
26
|
+
if (ipAddressRouting === 'LOGIN') {
|
|
27
|
+
return <ForbiddenPage />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ipAddressRouting === 'CONFIGURE') {
|
|
31
|
+
redirect('/admin/servers?setup=1');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(ipAddressRouting)) {
|
|
35
|
+
return <IpAddressDomainChooser servers={ipAddressRouting} />;
|
|
36
|
+
}
|
|
13
37
|
|
|
14
38
|
const [{ publicUrl }, isAdmin, { agents, folders, homepageMessage, currentUser }] = await Promise.all([
|
|
15
39
|
$provideServer(),
|
|
@@ -32,3 +56,79 @@ export default async function HomePage() {
|
|
|
32
56
|
</div>
|
|
33
57
|
);
|
|
34
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves special raw-IP behavior for standalone VPS access.
|
|
62
|
+
*
|
|
63
|
+
* @param host - Request host header.
|
|
64
|
+
* @returns Routing instruction or `null` when normal homepage rendering should continue.
|
|
65
|
+
*/
|
|
66
|
+
async function resolveIpAddressRouting(host: string | null): Promise<'LOGIN' | 'CONFIGURE' | ReadonlyArray<ServerRecord> | null> {
|
|
67
|
+
if (!host || !isIpAddressHost(host)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const registeredServers = await listRegisteredServersUsingServiceRole().catch(() => []);
|
|
72
|
+
if (resolveRegisteredServerByHost(host, registeredServers)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (registeredServers.length === 0) {
|
|
77
|
+
return (await isUserGlobalAdmin()) ? 'CONFIGURE' : 'LOGIN';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (registeredServers.length === 1) {
|
|
81
|
+
redirect(createServerPublicUrl(registeredServers[0]!.domain).href);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return registeredServers;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks whether the request host is a raw IPv4 or IPv6 address.
|
|
89
|
+
*
|
|
90
|
+
* @param host - Raw host header.
|
|
91
|
+
* @returns `true` for IP-address access.
|
|
92
|
+
*/
|
|
93
|
+
function isIpAddressHost(host: string): boolean {
|
|
94
|
+
const hostname = host
|
|
95
|
+
.trim()
|
|
96
|
+
.replace(/^\[(.+)\](?::\d+)?$/u, '$1')
|
|
97
|
+
.replace(/:\d+$/u, '');
|
|
98
|
+
|
|
99
|
+
return /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(hostname) || hostname.includes(':');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Simple domain chooser shown when a raw IP has multiple configured domains.
|
|
104
|
+
*
|
|
105
|
+
* @param props - Configured server list.
|
|
106
|
+
*/
|
|
107
|
+
function IpAddressDomainChooser(props: { readonly servers: ReadonlyArray<ServerRecord> }) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="min-h-screen bg-slate-50 px-4 py-24">
|
|
110
|
+
<div className="mx-auto max-w-2xl space-y-6">
|
|
111
|
+
<div>
|
|
112
|
+
<h1 className="text-3xl font-light text-slate-900">Choose a domain</h1>
|
|
113
|
+
<p className="mt-2 text-sm text-slate-600">
|
|
114
|
+
This VPS has multiple Agents Server domains configured. Open the domain you want to use.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="grid gap-3">
|
|
118
|
+
{props.servers.map((server) => {
|
|
119
|
+
const url = createServerPublicUrl(server.domain).href;
|
|
120
|
+
return (
|
|
121
|
+
<Link
|
|
122
|
+
key={server.id}
|
|
123
|
+
href={url}
|
|
124
|
+
className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-800 shadow-sm transition hover:border-blue-300 hover:text-blue-700"
|
|
125
|
+
>
|
|
126
|
+
{server.domain}
|
|
127
|
+
</Link>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|