@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
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { Plus } from 'lucide-react';
4
4
  import { useSearchParams } from 'next/navigation';
5
- import { useCallback, useEffect } from 'react';
5
+ import { useCallback, useEffect, useMemo } from 'react';
6
6
  import { Card } from '../../../components/Homepage/Card';
7
7
  import { Section } from '../../../components/Homepage/Section';
8
8
  import { useUnsavedChangesGuard } from '../../../components/utils/useUnsavedChangesGuard';
@@ -35,6 +35,7 @@ export function ServersClient() {
35
35
  deletingServerId,
36
36
  error,
37
37
  hasDirtyServerDrafts,
38
+ isStandaloneVps,
38
39
  isServerDraftDirty,
39
40
  loading,
40
41
  migrateServer,
@@ -52,6 +53,10 @@ export function ServersClient() {
52
53
  onServerCreated: reloadServers,
53
54
  });
54
55
  const hasUnsavedChanges = hasDirtyServerDrafts || (createServerWizard.isDialogOpen && createServerWizard.isDirty);
56
+ const serversWithDnsIssues = useMemo(
57
+ () => servers.filter((server) => server.dnsDiagnostic && server.dnsDiagnostic.status !== 'verified'),
58
+ [servers],
59
+ );
55
60
  const { confirmBeforeNavigation, allowNextNavigation } = useUnsavedChangesGuard({
56
61
  hasUnsavedChanges,
57
62
  preventInAppNavigation: true,
@@ -117,11 +122,28 @@ export function ServersClient() {
117
122
  </Card>
118
123
  ) : null}
119
124
 
125
+ {isStandaloneVps && serversWithDnsIssues.length > 0 ? (
126
+ <Card className="border-amber-200 bg-amber-50 hover:border-amber-200 hover:shadow-md">
127
+ <div className="space-y-2 text-sm text-amber-900">
128
+ <p className="font-semibold">
129
+ DNS setup is incomplete for {serversWithDnsIssues.length}{' '}
130
+ {serversWithDnsIssues.length === 1 ? 'domain' : 'domains'}.
131
+ </p>
132
+ <p>
133
+ Automatic nginx and SSL setup already ran, but these domains do not resolve to this VPS yet.
134
+ Review the DNS instructions in the affected server rows below, update the records at your DNS
135
+ provider, and refresh this page after propagation.
136
+ </p>
137
+ </div>
138
+ </Card>
139
+ ) : null}
140
+
120
141
  <Section title="Registered servers" gridClassName="grid gap-6">
121
142
  <Card className="hover:border-gray-200 hover:shadow-md">
122
143
  <ServersRegistryTable
123
144
  currentServerId={currentServerId}
124
145
  canEdit={canEdit}
146
+ isStandaloneVps={isStandaloneVps}
125
147
  loading={loading}
126
148
  migratingServerId={migratingServerId}
127
149
  navigatingServerId={navigatingServerId}
@@ -139,27 +161,17 @@ export function ServersClient() {
139
161
 
140
162
  {canEdit ? (
141
163
  <CreateServerDialog
142
- addAdditionalUser={createServerWizard.addAdditionalUser}
143
- derivedWizardTablePrefix={createServerWizard.derivedWizardTablePrefix}
144
164
  handleCreateServer={createServerWizard.handleCreateServer}
145
165
  handleIconUpload={createServerWizard.handleIconUpload}
146
- handleWizardBack={createServerWizard.handleWizardBack}
147
- handleWizardNext={createServerWizard.handleWizardNext}
148
- handleWizardStepSelection={createServerWizard.handleWizardStepSelection}
149
166
  iconInputRef={createServerWizard.iconInputRef}
150
167
  isCreatingServer={createServerWizard.isCreatingServer}
151
168
  isOpen={createServerWizard.isDialogOpen}
152
169
  isUploadingIcon={createServerWizard.isUploadingIcon}
153
- removeAdditionalUser={createServerWizard.removeAdditionalUser}
154
170
  requestClose={createServerWizard.requestClose}
155
171
  resetWizard={createServerWizard.resetWizard}
156
- updateAdditionalUser={createServerWizard.updateAdditionalUser}
157
- updateAdminUser={createServerWizard.updateAdminUser}
158
- updateInitialSetting={createServerWizard.updateInitialSetting}
159
172
  updateWizardField={createServerWizard.updateWizardField}
160
173
  wizardError={createServerWizard.wizardError}
161
174
  wizardState={createServerWizard.wizardState}
162
- wizardStep={createServerWizard.wizardStep}
163
175
  />
164
176
  ) : null}
165
177
 
@@ -22,6 +22,11 @@ type ManagedServersReadResponse = {
22
22
  */
23
23
  readonly canEdit: boolean;
24
24
 
25
+ /**
26
+ * Whether the registry is backed by standalone VPS `SERVERS` configuration.
27
+ */
28
+ readonly isStandaloneVps?: boolean;
29
+
25
30
  /**
26
31
  * Optional failure message returned by the API.
27
32
  */
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Supported DNS verification states shown for standalone VPS domains.
3
+ *
4
+ * @private shared type for the `/admin/servers` registry flow
5
+ */
6
+ export type ManagedServerDnsStatus = 'verified' | 'pending' | 'misconfigured' | 'unavailable';
7
+
8
+ /**
9
+ * One DNS record instruction rendered in the admin UI.
10
+ *
11
+ * @private shared type for the `/admin/servers` registry flow
12
+ */
13
+ export type ManagedServerDnsExpectedRecord = {
14
+ /**
15
+ * DNS record type.
16
+ */
17
+ readonly type: 'A' | 'AAAA' | 'CNAME';
18
+
19
+ /**
20
+ * Hostname/record name that should be configured.
21
+ */
22
+ readonly name: string;
23
+
24
+ /**
25
+ * Expected record target value.
26
+ */
27
+ readonly value: string;
28
+
29
+ /**
30
+ * Optional note clarifying when to use the record.
31
+ */
32
+ readonly note: string | null;
33
+ };
34
+
35
+ /**
36
+ * One external DNS-provider help link shown with the setup guidance.
37
+ *
38
+ * @private shared type for the `/admin/servers` registry flow
39
+ */
40
+ export type ManagedServerDnsProviderGuide = {
41
+ /**
42
+ * Human-readable provider label.
43
+ */
44
+ readonly label: string;
45
+
46
+ /**
47
+ * Official provider help URL.
48
+ */
49
+ readonly href: string;
50
+ };
51
+
52
+ /**
53
+ * DNS diagnostic payload returned for one standalone VPS domain.
54
+ *
55
+ * @private shared type for the `/admin/servers` registry flow
56
+ */
57
+ export type ManagedServerDnsDiagnostic = {
58
+ /**
59
+ * Overall DNS verification state for the domain.
60
+ */
61
+ readonly status: ManagedServerDnsStatus;
62
+
63
+ /**
64
+ * Short human-readable explanation of the current state.
65
+ */
66
+ readonly summary: string;
67
+
68
+ /**
69
+ * Public IP address expected for direct DNS records.
70
+ */
71
+ readonly publicIpAddress: string | null;
72
+
73
+ /**
74
+ * Addresses currently returned by DNS for the configured domain.
75
+ */
76
+ readonly resolvedAddresses: ReadonlyArray<string>;
77
+
78
+ /**
79
+ * DNS records that the user can add at their provider.
80
+ */
81
+ readonly expectedRecords: ReadonlyArray<ManagedServerDnsExpectedRecord>;
82
+
83
+ /**
84
+ * Provider documentation links for updating DNS records.
85
+ */
86
+ readonly providerGuides: ReadonlyArray<ManagedServerDnsProviderGuide>;
87
+ };
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { Fragment } from 'react';
3
4
  import { ArrowRightLeft, Loader2, RefreshCcw, Save } from 'lucide-react';
4
5
  import moment from 'moment';
5
6
  import {
@@ -55,6 +56,11 @@ type ServersRegistryTableProps = {
55
56
  */
56
57
  readonly loading: boolean;
57
58
 
59
+ /**
60
+ * Whether rows are standalone VPS domains instead of database `_Server` records.
61
+ */
62
+ readonly isStandaloneVps: boolean;
63
+
58
64
  /**
59
65
  * Server id currently running migrations.
60
66
  */
@@ -116,6 +122,7 @@ type ServersRegistryTableRowProps = {
116
122
  readonly currentServerId: number | null;
117
123
  readonly draft: ServerDraft | undefined;
118
124
  readonly isDirty: boolean;
125
+ readonly isStandaloneVps: boolean;
119
126
  readonly isMigrating: boolean;
120
127
  readonly isNavigating: boolean;
121
128
  readonly isSaving: boolean;
@@ -172,6 +179,7 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
172
179
  canEdit,
173
180
  draft,
174
181
  isDirty,
182
+ isStandaloneVps,
175
183
  isMigrating,
176
184
  isNavigating,
177
185
  isSaving,
@@ -182,109 +190,213 @@ function ServersRegistryTableRow(props: ServersRegistryTableRowProps) {
182
190
  server,
183
191
  } = props;
184
192
  const isCurrent = server.id === currentServerId;
193
+ const dnsDiagnostic = server.dnsDiagnostic || null;
194
+ const hasDnsIssue = Boolean(dnsDiagnostic && dnsDiagnostic.status !== 'verified');
195
+ const columnCount = isStandaloneVps ? 6 : 8;
185
196
 
186
197
  return (
187
- <tr className={isCurrent ? 'bg-blue-50/40' : 'hover:bg-gray-50'}>
188
- <td className="px-4 py-3 align-top">
189
- <input
190
- type="text"
191
- value={draft?.name || ''}
192
- onChange={(event) => onUpdateServerDraft(server.id, 'name', event.target.value)}
193
- className={INPUT_CLASS_NAME}
194
- disabled={!canEdit}
195
- aria-label={`Server name for ${server.name}`}
196
- />
197
- </td>
198
- <td className="px-4 py-3 align-top">
199
- <select
200
- value={draft?.environment || server.environment}
201
- onChange={(event) =>
202
- onUpdateServerDraft(server.id, 'environment', event.target.value as ManagedServerEnvironment)
203
- }
204
- className={INPUT_CLASS_NAME}
205
- disabled={!canEdit}
206
- aria-label={`Environment for ${server.name}`}
207
- >
208
- {MANAGED_SERVER_ENVIRONMENT_OPTIONS.map((environment) => (
209
- <option key={environment} value={environment}>
210
- {environment}
211
- </option>
212
- ))}
213
- </select>
214
- </td>
215
- <td className="px-4 py-3 align-top">
216
- <input
217
- type="text"
218
- value={draft?.domain || ''}
219
- onChange={(event) => onUpdateServerDraft(server.id, 'domain', event.target.value)}
220
- className={INPUT_CLASS_NAME}
221
- disabled={!canEdit}
222
- aria-label={`Domain for ${server.name}`}
223
- />
224
- </td>
225
- <td className="px-4 py-3 align-top">
226
- <input
227
- type="text"
228
- value={draft?.tablePrefix || ''}
229
- onChange={(event) => onUpdateServerDraft(server.id, 'tablePrefix', event.target.value)}
230
- className={`${INPUT_CLASS_NAME} font-mono`}
231
- disabled={!canEdit}
232
- aria-label={`Table prefix for ${server.name}`}
233
- />
234
- </td>
235
- <td className="px-4 py-3 align-top">
236
- <div className="flex flex-wrap gap-2">
237
- {isCurrent ? <ServerStatusBadge label="Current" tone="green" /> : null}
238
- {isDirty ? <ServerStatusBadge label="Unsaved" tone="blue" /> : null}
239
- {!isCurrent && !isDirty ? <span className="text-xs text-gray-400">-</span> : null}
240
- </div>
241
- </td>
242
- <td className="px-4 py-3 align-top text-xs text-gray-600">
243
- <span className="whitespace-nowrap font-mono">{formatDateTime(server.createdAt)}</span>
244
- </td>
245
- <td className="px-4 py-3 align-top text-xs text-gray-600">
246
- <span className="whitespace-nowrap font-mono">{formatDateTime(server.updatedAt)}</span>
247
- </td>
248
- <td className="px-4 py-3 align-top">
249
- <div className="flex flex-wrap justify-end gap-2">
250
- <button
251
- type="button"
252
- onClick={() => void onSaveServer(server.id)}
253
- disabled={!canEdit || !isDirty || isSaving}
254
- className={`${PRIMARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
255
- >
256
- {isSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Save className="h-3.5 w-3.5" />}
257
- Save
258
- </button>
259
- <button
260
- type="button"
261
- onClick={() => void onMigrateServer(server.id)}
262
- disabled={!canEdit || isMigrating}
263
- className={`${SECONDARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
264
- >
265
- {isMigrating ? (
266
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
267
- ) : (
268
- <RefreshCcw className="h-3.5 w-3.5" />
269
- )}
270
- Migrate
271
- </button>
272
- <button
273
- type="button"
274
- onClick={() => void onSwitchServer(server)}
275
- disabled={isNavigating}
276
- className={`${SECONDARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
277
- >
278
- {isNavigating ? (
279
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
280
- ) : (
281
- <ArrowRightLeft className="h-3.5 w-3.5" />
282
- )}
283
- Switch
284
- </button>
285
- </div>
286
- </td>
287
- </tr>
198
+ <>
199
+ <tr className={isCurrent ? 'bg-blue-50/40' : 'hover:bg-gray-50'}>
200
+ <td className="px-4 py-3 align-top">
201
+ <input
202
+ type="text"
203
+ value={draft?.name || ''}
204
+ onChange={(event) => onUpdateServerDraft(server.id, 'name', event.target.value)}
205
+ className={INPUT_CLASS_NAME}
206
+ disabled={!canEdit}
207
+ aria-label={`Server name for ${server.name}`}
208
+ />
209
+ </td>
210
+ {!isStandaloneVps ? (
211
+ <td className="px-4 py-3 align-top">
212
+ <select
213
+ value={draft?.environment || server.environment}
214
+ onChange={(event) =>
215
+ onUpdateServerDraft(
216
+ server.id,
217
+ 'environment',
218
+ event.target.value as ManagedServerEnvironment,
219
+ )
220
+ }
221
+ className={INPUT_CLASS_NAME}
222
+ disabled={!canEdit}
223
+ aria-label={`Environment for ${server.name}`}
224
+ >
225
+ {MANAGED_SERVER_ENVIRONMENT_OPTIONS.map((environment) => (
226
+ <option key={environment} value={environment}>
227
+ {environment}
228
+ </option>
229
+ ))}
230
+ </select>
231
+ </td>
232
+ ) : null}
233
+ <td className="px-4 py-3 align-top">
234
+ <input
235
+ type="text"
236
+ value={draft?.domain || ''}
237
+ onChange={(event) => onUpdateServerDraft(server.id, 'domain', event.target.value)}
238
+ className={INPUT_CLASS_NAME}
239
+ disabled={!canEdit}
240
+ aria-label={`Domain for ${server.name}`}
241
+ />
242
+ </td>
243
+ {!isStandaloneVps ? (
244
+ <td className="px-4 py-3 align-top">
245
+ <input
246
+ type="text"
247
+ value={draft?.tablePrefix || ''}
248
+ onChange={(event) => onUpdateServerDraft(server.id, 'tablePrefix', event.target.value)}
249
+ className={`${INPUT_CLASS_NAME} font-mono`}
250
+ disabled={!canEdit}
251
+ aria-label={`Table prefix for ${server.name}`}
252
+ />
253
+ </td>
254
+ ) : null}
255
+ <td className="px-4 py-3 align-top">
256
+ <div className="flex flex-wrap gap-2">
257
+ {isCurrent ? <ServerStatusBadge label="Current" tone="green" /> : null}
258
+ {isDirty ? <ServerStatusBadge label="Unsaved" tone="blue" /> : null}
259
+ {dnsDiagnostic?.status === 'verified' ? (
260
+ <ServerStatusBadge label="DNS ready" tone="green" />
261
+ ) : null}
262
+ {hasDnsIssue ? <ServerStatusBadge label="DNS issue" tone="amber" /> : null}
263
+ {!isCurrent && !isDirty && !dnsDiagnostic ? <span className="text-xs text-gray-400">-</span> : null}
264
+ </div>
265
+ </td>
266
+ <td className="px-4 py-3 align-top text-xs text-gray-600">
267
+ <span className="whitespace-nowrap font-mono">{formatDateTime(server.createdAt)}</span>
268
+ </td>
269
+ <td className="px-4 py-3 align-top text-xs text-gray-600">
270
+ <span className="whitespace-nowrap font-mono">{formatDateTime(server.updatedAt)}</span>
271
+ </td>
272
+ <td className="px-4 py-3 align-top">
273
+ <div className="flex flex-wrap justify-end gap-2">
274
+ <button
275
+ type="button"
276
+ onClick={() => void onSaveServer(server.id)}
277
+ disabled={!canEdit || !isDirty || isSaving}
278
+ className={`${PRIMARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
279
+ >
280
+ {isSaving ? (
281
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
282
+ ) : (
283
+ <Save className="h-3.5 w-3.5" />
284
+ )}
285
+ Save
286
+ </button>
287
+ <button
288
+ type="button"
289
+ onClick={() => void onMigrateServer(server.id)}
290
+ disabled={!canEdit || isMigrating}
291
+ className={`${SECONDARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
292
+ >
293
+ {isMigrating ? (
294
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
295
+ ) : (
296
+ <RefreshCcw className="h-3.5 w-3.5" />
297
+ )}
298
+ Migrate
299
+ </button>
300
+ <button
301
+ type="button"
302
+ onClick={() => void onSwitchServer(server)}
303
+ disabled={isNavigating}
304
+ className={`${SECONDARY_BUTTON_CLASS_NAME} px-2 py-1 text-xs`}
305
+ >
306
+ {isNavigating ? (
307
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
308
+ ) : (
309
+ <ArrowRightLeft className="h-3.5 w-3.5" />
310
+ )}
311
+ Switch
312
+ </button>
313
+ </div>
314
+ </td>
315
+ </tr>
316
+ {hasDnsIssue ? (
317
+ <tr className="bg-amber-50/70">
318
+ <td colSpan={columnCount} className="px-4 py-4">
319
+ <div className="space-y-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-4 text-sm text-amber-900">
320
+ <div className="space-y-1">
321
+ <p className="font-semibold">
322
+ DNS setup needs attention for <span className="font-mono">{server.domain}</span>
323
+ </p>
324
+ <p>{dnsDiagnostic?.summary}</p>
325
+ {dnsDiagnostic?.resolvedAddresses.length ? (
326
+ <p className="text-xs text-amber-800">
327
+ Currently resolves to:{' '}
328
+ <span className="font-mono">{dnsDiagnostic.resolvedAddresses.join(', ')}</span>
329
+ </p>
330
+ ) : null}
331
+ </div>
332
+
333
+ {dnsDiagnostic?.expectedRecords.length ? (
334
+ <div className="overflow-x-auto rounded-lg border border-amber-200 bg-white">
335
+ <table className="min-w-full divide-y divide-amber-100 text-xs">
336
+ <thead className="bg-amber-100/60 text-amber-900">
337
+ <tr>
338
+ <th className="px-3 py-2 text-left font-semibold">Type</th>
339
+ <th className="px-3 py-2 text-left font-semibold">Name</th>
340
+ <th className="px-3 py-2 text-left font-semibold">Value</th>
341
+ <th className="px-3 py-2 text-left font-semibold">When to use</th>
342
+ </tr>
343
+ </thead>
344
+ <tbody className="divide-y divide-amber-100">
345
+ {dnsDiagnostic.expectedRecords.map((record) => (
346
+ <tr key={`${record.type}-${record.name}-${record.value}`}>
347
+ <td className="px-3 py-2 font-mono font-semibold text-amber-900">
348
+ {record.type}
349
+ </td>
350
+ <td className="px-3 py-2 font-mono text-slate-800">
351
+ {record.name}
352
+ </td>
353
+ <td className="px-3 py-2 font-mono text-slate-800">
354
+ {record.value}
355
+ </td>
356
+ <td className="px-3 py-2 text-amber-900">
357
+ {record.note || 'Required.'}
358
+ </td>
359
+ </tr>
360
+ ))}
361
+ </tbody>
362
+ </table>
363
+ </div>
364
+ ) : null}
365
+
366
+ <div className="space-y-2">
367
+ <p className="font-medium">How to fix it</p>
368
+ <ol className="list-decimal space-y-1 pl-5 text-amber-900">
369
+ <li>Open your DNS provider for this domain.</li>
370
+ <li>
371
+ Add one of the records above for{' '}
372
+ <span className="font-mono">{server.domain}</span>.
373
+ </li>
374
+ <li>Remove conflicting A, AAAA, or CNAME records for the same hostname.</li>
375
+ <li>Wait for DNS propagation, then refresh this page.</li>
376
+ </ol>
377
+ </div>
378
+
379
+ <div className="space-y-2">
380
+ <p className="font-medium">Provider guides</p>
381
+ <div className="flex flex-wrap gap-3 text-xs">
382
+ {dnsDiagnostic?.providerGuides.map((guide) => (
383
+ <a
384
+ key={guide.href}
385
+ href={guide.href}
386
+ target="_blank"
387
+ rel="noreferrer"
388
+ className="font-semibold text-amber-700 underline decoration-amber-400 underline-offset-2 hover:text-amber-900"
389
+ >
390
+ {guide.label}
391
+ </a>
392
+ ))}
393
+ </div>
394
+ </div>
395
+ </div>
396
+ </td>
397
+ </tr>
398
+ ) : null}
399
+ </>
288
400
  );
289
401
  }
290
402
 
@@ -300,6 +412,7 @@ export function ServersRegistryTable(props: ServersRegistryTableProps) {
300
412
  const {
301
413
  currentServerId,
302
414
  canEdit,
415
+ isStandaloneVps,
303
416
  isServerDraftDirty,
304
417
  loading,
305
418
  migratingServerId,
@@ -329,22 +442,37 @@ export function ServersRegistryTable(props: ServersRegistryTableProps) {
329
442
  ) : (
330
443
  <div className="mt-4 overflow-x-auto rounded-xl border border-gray-200">
331
444
  <table className="min-w-full table-fixed divide-y divide-gray-200 text-sm">
332
- <colgroup>
333
- <col className="w-[16rem]" />
334
- <col className="w-[10rem]" />
335
- <col className="w-[18rem]" />
336
- <col className="w-[13rem]" />
337
- <col className="w-[10rem]" />
338
- <col className="w-[11rem]" />
339
- <col className="w-[11rem]" />
340
- <col className="w-[12rem]" />
341
- </colgroup>
445
+ {isStandaloneVps ? (
446
+ <colgroup>
447
+ <col className="w-[16rem]" />
448
+ <col className="w-[18rem]" />
449
+ <col className="w-[10rem]" />
450
+ <col className="w-[11rem]" />
451
+ <col className="w-[11rem]" />
452
+ <col className="w-[12rem]" />
453
+ </colgroup>
454
+ ) : (
455
+ <colgroup>
456
+ <col className="w-[16rem]" />
457
+ <col className="w-[10rem]" />
458
+ <col className="w-[18rem]" />
459
+ <col className="w-[13rem]" />
460
+ <col className="w-[10rem]" />
461
+ <col className="w-[11rem]" />
462
+ <col className="w-[11rem]" />
463
+ <col className="w-[12rem]" />
464
+ </colgroup>
465
+ )}
342
466
  <thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-500">
343
467
  <tr>
344
468
  <th className="px-4 py-3 text-left font-semibold">Name</th>
345
- <th className="px-4 py-3 text-left font-semibold">Environment</th>
469
+ {!isStandaloneVps ? (
470
+ <th className="px-4 py-3 text-left font-semibold">Environment</th>
471
+ ) : null}
346
472
  <th className="px-4 py-3 text-left font-semibold">Domain</th>
347
- <th className="px-4 py-3 text-left font-semibold">Table prefix</th>
473
+ {!isStandaloneVps ? (
474
+ <th className="px-4 py-3 text-left font-semibold">Table prefix</th>
475
+ ) : null}
348
476
  <th className="px-4 py-3 text-left font-semibold">Status</th>
349
477
  <th className="px-4 py-3 text-left font-semibold">Created</th>
350
478
  <th className="px-4 py-3 text-left font-semibold">Updated</th>
@@ -353,21 +481,23 @@ export function ServersRegistryTable(props: ServersRegistryTableProps) {
353
481
  </thead>
354
482
  <tbody className="divide-y divide-gray-200 bg-white">
355
483
  {servers.map((server) => (
356
- <ServersRegistryTableRow
357
- key={server.id}
358
- currentServerId={currentServerId}
359
- canEdit={canEdit}
360
- draft={serverDrafts[server.id]}
361
- isDirty={isServerDraftDirty(server)}
362
- isMigrating={migratingServerId === server.id}
363
- isNavigating={navigatingServerId === server.id}
364
- isSaving={savingServerId === server.id}
365
- onMigrateServer={onMigrateServer}
366
- onSaveServer={onSaveServer}
367
- onSwitchServer={onSwitchServer}
368
- onUpdateServerDraft={onUpdateServerDraft}
369
- server={server}
370
- />
484
+ <Fragment key={server.id}>
485
+ <ServersRegistryTableRow
486
+ currentServerId={currentServerId}
487
+ canEdit={canEdit}
488
+ draft={serverDrafts[server.id]}
489
+ isDirty={isServerDraftDirty(server)}
490
+ isStandaloneVps={isStandaloneVps}
491
+ isMigrating={migratingServerId === server.id}
492
+ isNavigating={navigatingServerId === server.id}
493
+ isSaving={savingServerId === server.id}
494
+ onMigrateServer={onMigrateServer}
495
+ onSaveServer={onSaveServer}
496
+ onSwitchServer={onSwitchServer}
497
+ onUpdateServerDraft={onUpdateServerDraft}
498
+ server={server}
499
+ />
500
+ </Fragment>
371
501
  ))}
372
502
  </tbody>
373
503
  </table>