@promptbook/cli 0.112.0-95 → 0.112.0-97

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.
Files changed (70) hide show
  1. package/apps/agents-server/README.md +3 -3
  2. package/apps/agents-server/next.config.ts +8 -1
  3. package/apps/agents-server/playwright.config.ts +4 -1
  4. package/apps/agents-server/src/app/admin/code-runners/CodeRunnersClient.tsx +358 -19
  5. package/apps/agents-server/src/app/admin/database/DatabaseAdminClient.tsx +38 -0
  6. package/apps/agents-server/src/app/admin/database/DatabaseAdminStudioSurface.tsx +42 -0
  7. package/apps/agents-server/src/app/admin/database/page.tsx +34 -0
  8. package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +46 -505
  9. package/apps/agents-server/src/app/admin/servers/ServersClient.tsx +23 -11
  10. package/apps/agents-server/src/app/admin/servers/ServersRegistryApi.ts +5 -0
  11. package/apps/agents-server/src/app/admin/servers/ServersRegistryDnsTypes.ts +87 -0
  12. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +258 -128
  13. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +46 -334
  14. package/apps/agents-server/src/app/admin/servers/useServersRegistryState.ts +26 -2
  15. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +435 -0
  16. package/apps/agents-server/src/app/admin/update/page.tsx +14 -0
  17. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +197 -0
  18. package/apps/agents-server/src/app/api/admin/code-runners/route.ts +4 -35
  19. package/apps/agents-server/src/app/api/admin/database/studio/route.ts +113 -0
  20. package/apps/agents-server/src/app/api/admin/servers/[serverId]/route.ts +10 -5
  21. package/apps/agents-server/src/app/api/admin/servers/route.ts +97 -6
  22. package/apps/agents-server/src/app/api/admin/update/route.ts +52 -0
  23. package/apps/agents-server/src/app/api/auth/login/route.ts +8 -0
  24. package/apps/agents-server/src/app/api/auth/logout/route.ts +10 -2
  25. package/apps/agents-server/src/app/layout.tsx +1 -0
  26. package/apps/agents-server/src/app/page.tsx +10 -0
  27. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +10 -0
  28. package/apps/agents-server/src/database/$provideClientSql.ts +4 -21
  29. package/apps/agents-server/src/database/$provideDatabaseAdminExecutor.ts +252 -0
  30. package/apps/agents-server/src/database/$providePostgresPool.ts +27 -0
  31. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +11 -1
  32. package/apps/agents-server/src/database/agentsServerDatabaseMode.ts +20 -1
  33. package/apps/agents-server/src/database/postgres/$provideLocalPostgresSupabase.ts +1261 -0
  34. package/apps/agents-server/src/database/resolvePostgresConnectionString.ts +26 -0
  35. package/apps/agents-server/src/database/sqlite/$provideAgentsServerSqliteDatabase.ts +83 -0
  36. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +20 -71
  37. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +2 -0
  38. package/apps/agents-server/src/languages/translations/czech.yaml +2 -0
  39. package/apps/agents-server/src/languages/translations/english.yaml +2 -0
  40. package/apps/agents-server/src/middleware.ts +32 -0
  41. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  42. package/apps/agents-server/src/utils/codeRunnerAuthentication.ts +394 -0
  43. package/apps/agents-server/src/utils/codeRunnerConfiguration.ts +67 -0
  44. package/apps/agents-server/src/utils/serverManagement/standaloneVpsServerMetadata.ts +145 -0
  45. package/apps/agents-server/src/utils/serverRegistry.ts +7 -6
  46. package/apps/agents-server/src/utils/session.ts +37 -9
  47. package/apps/agents-server/src/utils/shibboleth/createShibbolethAuthenticationLogPayload.ts +173 -0
  48. package/apps/agents-server/src/utils/shibboleth/writeShibbolethAuthenticationLog.ts +27 -0
  49. package/apps/agents-server/src/utils/standaloneVpsDnsDiagnostics.ts +258 -0
  50. package/apps/agents-server/src/utils/standaloneVpsRawIpBootstrap.ts +87 -0
  51. package/apps/agents-server/src/utils/vpsConfiguration.ts +87 -15
  52. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +664 -0
  53. package/esm/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +9 -1
  54. package/esm/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  55. package/esm/index.es.js +8 -6
  56. package/esm/index.es.js.map +1 -1
  57. package/esm/src/version.d.ts +1 -1
  58. package/package.json +2 -1
  59. package/src/book-components/Chat/utils/renderMarkdown.ts +1 -3
  60. package/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.ts +1 -1
  61. package/src/other/templates/getTemplatesPipelineCollection.ts +767 -745
  62. package/src/scrapers/document/DocumentScraper.ts +1 -1
  63. package/src/scrapers/document-legacy/LegacyDocumentScraper.ts +1 -1
  64. package/src/version.ts +2 -2
  65. package/src/versions.txt +2 -1
  66. package/umd/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +9 -1
  67. package/umd/apps/agents-server/src/utils/serverRegistry.d.ts +1 -1
  68. package/umd/index.umd.js +8 -6
  69. package/umd/index.umd.js.map +1 -1
  70. package/umd/src/version.d.ts +1 -1
@@ -1,15 +1,11 @@
1
- import { execFile } from 'child_process';
2
- import { promisify } from 'util';
3
1
  import { NextResponse } from 'next/server';
4
2
  import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
3
+ import { readConfiguredCodeRunner, resolveCodeRunnerStatus } from '@/src/utils/codeRunnerConfiguration';
5
4
  import {
6
5
  applyVpsCodeRunnerConfiguration,
7
- listVpsEnvironmentVariables,
8
6
  updateVpsEnvironmentVariables,
9
7
  } from '@/src/utils/vpsConfiguration';
10
8
 
11
- const execFileAsync = promisify(execFile);
12
-
13
9
  /**
14
10
  * Loads configured code-runner settings from the editable VPS environment.
15
11
  */
@@ -19,16 +15,11 @@ export async function GET() {
19
15
  }
20
16
 
21
17
  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>;
18
+ const configuredCodeRunner = await readConfiguredCodeRunner();
26
19
 
27
20
  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'),
21
+ ...configuredCodeRunner,
22
+ status: await resolveCodeRunnerStatus(configuredCodeRunner.agent),
32
23
  });
33
24
  } catch (error) {
34
25
  return NextResponse.json(
@@ -80,25 +71,3 @@ export async function PATCH(request: Request) {
80
71
  );
81
72
  }
82
73
  }
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { isAgentsServerSqliteMode } from '../../../../../database/agentsServerDatabaseMode';
2
+ import { isAgentsServerStandaloneMode } from '../../../../../database/agentsServerDatabaseMode';
3
3
  import { resolveCurrentServerRegistryContext } from '../../../../../utils/currentServerRegistryContext';
4
4
  import { isUserGlobalAdmin } from '../../../../../utils/isUserGlobalAdmin';
5
5
  import {
@@ -15,6 +15,7 @@ import {
15
15
  updateManagedServer,
16
16
  type UpdateServerInput,
17
17
  } from '../../../../../utils/serverManagement';
18
+ import { applyStandaloneVpsServerMetadata } from '../../../../../utils/serverManagement/standaloneVpsServerMetadata';
18
19
  import {
19
20
  applyVpsRuntimeConfiguration,
20
21
  listConfiguredVpsDomains,
@@ -36,8 +37,12 @@ export async function PATCH(request: Request, context: { params: Promise<{ serve
36
37
  const body = (await request.json()) as Omit<UpdateServerInput, 'id'>;
37
38
  const parsedServerId = parseManagedServerId(serverId);
38
39
 
39
- if (isAgentsServerSqliteMode()) {
40
+ if (isAgentsServerStandaloneMode()) {
40
41
  const updatedServer = await updateStandaloneVpsServerDomain(parsedServerId, body.domain);
42
+ await applyStandaloneVpsServerMetadata({
43
+ tablePrefix: updatedServer.tablePrefix,
44
+ name: body.name,
45
+ });
41
46
  return NextResponse.json({ server: updatedServer });
42
47
  }
43
48
 
@@ -71,7 +76,7 @@ export async function DELETE(_request: Request, context: { params: Promise<{ ser
71
76
  const { serverId } = await context.params;
72
77
  const parsedServerId = parseManagedServerId(serverId);
73
78
 
74
- if (isAgentsServerSqliteMode()) {
79
+ if (isAgentsServerStandaloneMode()) {
75
80
  await deleteStandaloneVpsServerDomain(parsedServerId);
76
81
  return NextResponse.json({
77
82
  success: true,
@@ -126,7 +131,7 @@ async function updateStandaloneVpsServerDomain(serverId: number, rawDomain: stri
126
131
  const domains = await listConfiguredVpsDomains();
127
132
  const nextDomains = domains.map((domain, index) => (index === serverIndex ? normalizedDomain : domain));
128
133
  await updateConfiguredVpsDomains(nextDomains);
129
- await applyVpsRuntimeConfiguration();
134
+ await applyVpsRuntimeConfiguration({ isProcessRestartEnabled: false });
130
135
 
131
136
  const updatedServer = listEnvironmentRegisteredServers().find((server) => server.domain === normalizedDomain);
132
137
  if (!updatedServer) {
@@ -150,5 +155,5 @@ async function deleteStandaloneVpsServerDomain(serverId: number): Promise<void>
150
155
 
151
156
  const domains = await listConfiguredVpsDomains();
152
157
  await updateConfiguredVpsDomains(domains.filter((_domain, index) => index !== serverIndex));
153
- await applyVpsRuntimeConfiguration();
158
+ await applyVpsRuntimeConfiguration({ isProcessRestartEnabled: false });
154
159
  }
@@ -1,8 +1,11 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { isAgentsServerSqliteMode } from '../../../../database/agentsServerDatabaseMode';
2
+ import { spaceTrim } from 'spacetrim';
3
+ import { DatabaseError } from '../../../../../../../src/errors/DatabaseError';
4
+ import { isAgentsServerStandaloneMode } from '../../../../database/agentsServerDatabaseMode';
3
5
  import { resolveCurrentServerRegistryContext } from '../../../../utils/currentServerRegistryContext';
4
6
  import { isUserAdmin } from '../../../../utils/isUserAdmin';
5
7
  import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
8
+ import { buildServerTablePrefix } from '../../../../utils/buildServerTablePrefix';
6
9
  import {
7
10
  createServerPublicUrl,
8
11
  listEnvironmentRegisteredServers,
@@ -14,6 +17,12 @@ import {
14
17
  resolveManagedServerErrorStatus,
15
18
  type CreateServerInput,
16
19
  } from '../../../../utils/serverManagement';
20
+ import { ManagedServerInputNormalizer } from '../../../../utils/serverManagement/ManagedServerInputNormalizer';
21
+ import {
22
+ applyStandaloneVpsServerMetadata,
23
+ resolveStandaloneVpsServerDisplayName,
24
+ } from '../../../../utils/serverManagement/standaloneVpsServerMetadata';
25
+ import { createStandaloneVpsDomainDnsDiagnostic } from '../../../../utils/standaloneVpsDnsDiagnostics';
17
26
  import {
18
27
  applyVpsRuntimeConfiguration,
19
28
  listConfiguredVpsDomains,
@@ -32,10 +41,15 @@ export async function GET() {
32
41
  }
33
42
 
34
43
  const context = await resolveCurrentServerRegistryContext();
44
+ const servers = isAgentsServerStandaloneMode()
45
+ ? await createStandaloneVpsServersResponse(context.registeredServers)
46
+ : context.registeredServers;
47
+
35
48
  return NextResponse.json({
36
- servers: context.registeredServers,
49
+ servers,
37
50
  currentServerId: context.currentServer?.id ?? null,
38
51
  canEdit: await isUserGlobalAdmin(),
52
+ isStandaloneVps: isAgentsServerStandaloneMode(),
39
53
  });
40
54
  } catch (error) {
41
55
  return NextResponse.json(
@@ -47,6 +61,30 @@ export async function GET() {
47
61
  }
48
62
  }
49
63
 
64
+ /**
65
+ * Enriches standalone VPS server rows with display metadata and DNS diagnostics.
66
+ *
67
+ * @param servers - Virtual standalone server rows.
68
+ * @returns Browser-safe server rows with DNS setup guidance.
69
+ */
70
+ async function createStandaloneVpsServersResponse(
71
+ servers: Awaited<ReturnType<typeof resolveCurrentServerRegistryContext>>['registeredServers'],
72
+ ) {
73
+ const primaryDomain = servers[0]?.domain ?? null;
74
+
75
+ return Promise.all(
76
+ servers.map(async (server) => ({
77
+ ...server,
78
+ name: await resolveStandaloneVpsServerDisplayName(server),
79
+ dnsDiagnostic: await createStandaloneVpsDomainDnsDiagnostic({
80
+ domain: server.domain,
81
+ publicIpAddress: process.env.PTBK_PUBLIC_IP_ADDRESS,
82
+ fallbackCnameTargetDomain: primaryDomain && primaryDomain !== server.domain ? primaryDomain : null,
83
+ }),
84
+ })),
85
+ );
86
+ }
87
+
50
88
  /**
51
89
  * Creates a new same-instance server.
52
90
  *
@@ -57,17 +95,25 @@ export async function POST(request: Request) {
57
95
  try {
58
96
  assertGlobalAdminAccess(await isUserGlobalAdmin());
59
97
 
60
- const body = (await request.json()) as CreateServerInput;
61
- if (isAgentsServerSqliteMode()) {
98
+ const body = withEnvironmentAdminUser((await request.json()) as CreateServerInput);
99
+ if (isAgentsServerStandaloneMode()) {
62
100
  const normalizedDomain = normalizeServerDomain(body.domain);
63
101
  if (!normalizedDomain) {
64
102
  return NextResponse.json({ error: 'A valid domain is required.' }, { status: 400 });
65
103
  }
104
+ const tablePrefix = normalizeStandaloneVpsCreateServerTablePrefix(body);
66
105
 
67
106
  const existingDomains = await listConfiguredVpsDomains();
68
- await updateConfiguredVpsDomains([...existingDomains, normalizedDomain]);
69
- await applyVpsRuntimeConfiguration();
107
+ await updateConfiguredVpsDomains([...existingDomains, normalizedDomain], { tablePrefix });
108
+ await applyVpsRuntimeConfiguration({ isProcessRestartEnabled: false });
70
109
  const createdServer = listEnvironmentRegisteredServers().find((server) => server.domain === normalizedDomain);
110
+ if (createdServer) {
111
+ await applyStandaloneVpsServerMetadata({
112
+ tablePrefix: createdServer.tablePrefix,
113
+ name: body.name,
114
+ iconUrl: body.iconUrl,
115
+ });
116
+ }
71
117
 
72
118
  return NextResponse.json(
73
119
  {
@@ -107,3 +153,48 @@ export async function POST(request: Request) {
107
153
  );
108
154
  }
109
155
  }
156
+
157
+ /**
158
+ * Uses the installer-managed environment admin when the browser no longer collects admin credentials.
159
+ *
160
+ * @param input - Raw create-server payload.
161
+ * @returns Payload compatible with the existing managed-server bootstrap flow.
162
+ */
163
+ function withEnvironmentAdminUser(input: CreateServerInput): CreateServerInput {
164
+ const adminPassword = process.env.ADMIN_PASSWORD || input.adminUser?.password || '';
165
+ const adminUsername = input.adminUser?.username?.trim() || 'admin';
166
+
167
+ return {
168
+ ...input,
169
+ adminUser: {
170
+ username: adminUsername,
171
+ password: adminPassword,
172
+ isAdmin: true,
173
+ },
174
+ additionalUsers: input.additionalUsers || [],
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Validates the generated server table prefix used by standalone VPS setup.
180
+ *
181
+ * @param input - Create-server payload with generated identifier and table prefix.
182
+ * @returns Validated server-level table prefix.
183
+ */
184
+ function normalizeStandaloneVpsCreateServerTablePrefix(input: CreateServerInput): string {
185
+ const identifier = ManagedServerInputNormalizer.normalizeServerIdentifier(input.identifier);
186
+ const tablePrefix = ManagedServerInputNormalizer.validateServerTablePrefix(input.tablePrefix);
187
+ const expectedTablePrefix = buildServerTablePrefix(identifier);
188
+
189
+ if (tablePrefix !== expectedTablePrefix) {
190
+ throw new DatabaseError(
191
+ spaceTrim(`
192
+ Table prefix \`${tablePrefix}\` does not match generated server identifier \`${identifier}\`.
193
+
194
+ Expected \`${expectedTablePrefix}\`.
195
+ `),
196
+ );
197
+ }
198
+
199
+ return tablePrefix;
200
+ }
@@ -0,0 +1,52 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { isUserGlobalAdmin } from '@/src/utils/isUserGlobalAdmin';
3
+ import { readVpsSelfUpdateOverview, startVpsSelfUpdate } from '@/src/utils/vpsSelfUpdate';
4
+
5
+ export const runtime = 'nodejs';
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ /**
9
+ * Loads the current standalone VPS self-update overview for the super-admin UI.
10
+ */
11
+ export async function GET() {
12
+ if (!(await isUserGlobalAdmin())) {
13
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
14
+ }
15
+
16
+ try {
17
+ return NextResponse.json(await readVpsSelfUpdateOverview());
18
+ } catch (error) {
19
+ return NextResponse.json(
20
+ { error: error instanceof Error ? error.message : 'Failed to load the update overview.' },
21
+ { status: 500 },
22
+ );
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Starts a detached standalone VPS self-update for the selected environment.
28
+ */
29
+ export async function POST(request: Request) {
30
+ if (!(await isUserGlobalAdmin())) {
31
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
32
+ }
33
+
34
+ try {
35
+ const body = (await request.json().catch(() => null)) as
36
+ | {
37
+ readonly environment?: string;
38
+ }
39
+ | null;
40
+
41
+ if (!body?.environment || typeof body.environment !== 'string') {
42
+ return NextResponse.json({ error: 'Update environment is required.' }, { status: 400 });
43
+ }
44
+
45
+ return NextResponse.json(await startVpsSelfUpdate(body.environment), { status: 202 });
46
+ } catch (error) {
47
+ return NextResponse.json(
48
+ { error: error instanceof Error ? error.message : 'Failed to start the update.' },
49
+ { status: 500 },
50
+ );
51
+ }
52
+ }
@@ -1,5 +1,6 @@
1
1
  import { authenticateUser } from '../../../../utils/authenticateUser';
2
2
  import { setSession } from '../../../../utils/session';
3
+ import { writeShibbolethAuthenticationLog } from '../../../../utils/shibboleth/writeShibbolethAuthenticationLog';
3
4
  import { NextResponse } from 'next/server';
4
5
 
5
6
  /**
@@ -7,6 +8,13 @@ import { NextResponse } from 'next/server';
7
8
  */
8
9
  export async function POST(request: Request) {
9
10
  try {
11
+ writeShibbolethAuthenticationLog(request.headers, {
12
+ event: 'login-route-request',
13
+ pathname: '/api/auth/login',
14
+ method: request.method,
15
+ hasSessionCookie: (request.headers.get('cookie') || '').includes('sessionToken='),
16
+ });
17
+
10
18
  const body = await request.json();
11
19
  const { username, password } = body;
12
20
 
@@ -1,10 +1,18 @@
1
- import { clearSession } from '../../../../utils/session';
2
1
  import { NextResponse } from 'next/server';
2
+ import { clearSession } from '../../../../utils/session';
3
+ import { writeShibbolethAuthenticationLog } from '../../../../utils/shibboleth/writeShibbolethAuthenticationLog';
3
4
 
4
5
  /**
5
6
  * Handles post.
6
7
  */
7
- export async function POST() {
8
+ export async function POST(request: Request) {
9
+ writeShibbolethAuthenticationLog(request.headers, {
10
+ event: 'logout-route-request',
11
+ pathname: '/api/auth/logout',
12
+ method: request.method,
13
+ hasSessionCookie: (request.headers.get('cookie') || '').includes('sessionToken='),
14
+ });
15
+
8
16
  await clearSession();
9
17
  return NextResponse.json({ success: true });
10
18
  }
@@ -46,6 +46,7 @@ import {
46
46
  CONTROL_PANEL_OPTION_AVAILABILITY_METADATA_KEYS,
47
47
  getControlPanelOptionAvailability,
48
48
  } from '../utils/getControlPanelOptionAvailability';
49
+ import '@prisma/studio-core/ui/index.css';
49
50
  import './globals.css';
50
51
 
51
52
  /**
@@ -13,6 +13,7 @@ import {
13
13
  resolveRegisteredServerByHost,
14
14
  type ServerRecord,
15
15
  } from '../utils/serverRegistry';
16
+ import { isStandaloneVpsRawIpBootstrapActive } from '../utils/standaloneVpsRawIpBootstrap';
16
17
  import { getHomePageAgents } from './_data/getHomePageAgents';
17
18
 
18
19
  /**
@@ -77,6 +78,15 @@ async function resolveIpAddressRouting(host: string | null): Promise<'LOGIN' | '
77
78
  return (await isUserGlobalAdmin()) ? 'CONFIGURE' : 'LOGIN';
78
79
  }
79
80
 
81
+ if (
82
+ isStandaloneVpsRawIpBootstrapActive({
83
+ nextPublicSiteUrl: process.env.NEXT_PUBLIC_SITE_URL,
84
+ publicIpAddress: process.env.PTBK_PUBLIC_IP_ADDRESS,
85
+ })
86
+ ) {
87
+ return null;
88
+ }
89
+
80
90
  if (registeredServers.length === 1) {
81
91
  redirect(createServerPublicUrl(registeredServers[0]!.domain).href);
82
92
  }
@@ -3,6 +3,7 @@ import {
3
3
  Code2,
4
4
  Globe2,
5
5
  KeyRound,
6
+ RefreshCw,
6
7
  Settings2,
7
8
  UserRound,
8
9
  Wrench,
@@ -202,6 +203,15 @@ export function buildHeaderSystemMenuItems({
202
203
  },
203
204
  ...(isGlobalAdmin
204
205
  ? [
206
+ {
207
+ label: translate('header.update'),
208
+ href: '/admin/update',
209
+ icon: RefreshCw,
210
+ } as SubMenuItem,
211
+ {
212
+ label: translate('header.database'),
213
+ href: '/admin/database',
214
+ } as SubMenuItem,
205
215
  {
206
216
  label: translate('header.logs'),
207
217
  href: '/admin/logs',
@@ -1,5 +1,5 @@
1
1
  import { $isRunningInNode } from '@promptbook-local/utils';
2
- import { Pool } from 'pg';
2
+ import { $providePostgresPool } from './$providePostgresPool';
3
3
 
4
4
  /**
5
5
  * SQL tagged-template executor used by server routes and utilities.
@@ -30,13 +30,6 @@ export type ClientSqlExecutor = ClientSql & {
30
30
  readonly raw: ClientSqlRaw;
31
31
  };
32
32
 
33
- /**
34
- * Shared PostgreSQL pool reused across all requests in the server process.
35
- *
36
- * @private internal singleton of Agents Server database layer
37
- */
38
- let clientPool: Pool | undefined;
39
-
40
33
  /**
41
34
  * Provides SQL tagged-template client for server-side PostgreSQL access.
42
35
  *
@@ -47,17 +40,7 @@ export async function $provideClientSql(): Promise<ClientSqlExecutor> {
47
40
  throw new Error('Function `$provideClientSql` can only be used in Node.js runtime.');
48
41
  }
49
42
 
50
- if (!clientPool) {
51
- const connectionString = process.env.POSTGRES_URL || process.env.DATABASE_URL;
52
- if (!connectionString) {
53
- throw new Error('Environment variable `POSTGRES_URL` or `DATABASE_URL` must be defined.');
54
- }
55
-
56
- clientPool = new Pool({
57
- connectionString,
58
- ssl: { rejectUnauthorized: false },
59
- });
60
- }
43
+ const clientPool = $providePostgresPool();
61
44
 
62
45
  const executeTemplate = async <TRow = Array<Record<string, unknown>>>(
63
46
  templateStrings: TemplateStringsArray,
@@ -72,7 +55,7 @@ export async function $provideClientSql(): Promise<ClientSqlExecutor> {
72
55
  }
73
56
 
74
57
  const text = textChunks.join('');
75
- const result = await clientPool!.query(text, [...templateValues]);
58
+ const result = await clientPool.query(text, [...templateValues]);
76
59
  return result.rows as TRow;
77
60
  };
78
61
 
@@ -80,7 +63,7 @@ export async function $provideClientSql(): Promise<ClientSqlExecutor> {
80
63
  text: string,
81
64
  values: ReadonlyArray<unknown> = [],
82
65
  ): Promise<TRow> => {
83
- const result = await clientPool!.query(text, [...values]);
66
+ const result = await clientPool.query(text, [...values]);
84
67
  return result.rows as TRow;
85
68
  };
86
69