@promptbook/cli 0.112.0-132 → 0.112.0-134

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 (101) hide show
  1. package/apps/agents-server/src/app/admin/api-tokens/ApiTokensClient.tsx +4 -1
  2. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +3 -0
  3. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackTable.tsx +13 -8
  4. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +3 -0
  5. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryTable.tsx +13 -8
  6. package/apps/agents-server/src/app/admin/custom-css/CustomCssClient.tsx +4 -0
  7. package/apps/agents-server/src/app/admin/custom-css/CustomCssEditorPanel.tsx +7 -1
  8. package/apps/agents-server/src/app/admin/custom-css/CustomCssFilesPanel.tsx +7 -1
  9. package/apps/agents-server/src/app/admin/custom-js/CustomJsClient.tsx +4 -0
  10. package/apps/agents-server/src/app/admin/custom-js/CustomJsEditorPanel.tsx +7 -1
  11. package/apps/agents-server/src/app/admin/custom-js/CustomJsFilesPanel.tsx +7 -1
  12. package/apps/agents-server/src/app/admin/files/FilesGalleryClient.tsx +4 -0
  13. package/apps/agents-server/src/app/admin/files/FilesGalleryGrid.tsx +9 -11
  14. package/apps/agents-server/src/app/admin/files/FilesGalleryTable.tsx +9 -8
  15. package/apps/agents-server/src/app/admin/images/ImagesGalleryClient.tsx +4 -0
  16. package/apps/agents-server/src/app/admin/images/ImagesGalleryGrid.tsx +16 -11
  17. package/apps/agents-server/src/app/admin/images/ImagesGalleryTable.tsx +18 -9
  18. package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +9 -9
  19. package/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +7 -6
  20. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +11 -10
  21. package/apps/agents-server/src/app/admin/task-manager/TaskManagerClient.tsx +4 -2
  22. package/apps/agents-server/src/app/admin/task-manager/TaskManagerTaskRow.tsx +14 -15
  23. package/apps/agents-server/src/app/admin/task-manager/TaskManagerTasksCard.tsx +4 -1
  24. package/apps/agents-server/src/app/admin/task-manager/useTaskManagerState.ts +6 -9
  25. package/apps/agents-server/src/app/admin/update/CustomCommitPicker.tsx +258 -0
  26. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +305 -108
  27. package/apps/agents-server/src/app/admin/usage/UsageClient.tsx +4 -2
  28. package/apps/agents-server/src/app/admin/usage/UsageClientAnalyticsPanels.tsx +6 -4
  29. package/apps/agents-server/src/app/admin/usage/UsageClientFormatting.ts +6 -12
  30. package/apps/agents-server/src/app/admin/usage/UsageClientTimelineChart.tsx +6 -3
  31. package/apps/agents-server/src/app/admin/usage/useUsageClientState.ts +12 -4
  32. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +8 -3
  33. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +6 -20
  34. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +2 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/book/useBookEditorHistory.ts +9 -2
  36. package/apps/agents-server/src/app/agents/[agentName]/chat/useAgentChatHistoryClientState.ts +2 -15
  37. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +6 -1
  38. package/apps/agents-server/src/app/agents/[agentName]/integration/CalendarIntegrationSection.tsx +23 -5
  39. package/apps/agents-server/src/app/agents/[agentName]/timeouts/AgentTimeoutsClient.tsx +4 -2
  40. package/apps/agents-server/src/app/agents/[agentName]/timeouts/AgentTimeoutsFiltersCard.tsx +9 -4
  41. package/apps/agents-server/src/app/agents/[agentName]/timeouts/AgentTimeoutsTableCard.tsx +4 -1
  42. package/apps/agents-server/src/app/agents/[agentName]/timeouts/AgentTimeoutsTableRow.tsx +10 -3
  43. package/apps/agents-server/src/app/api/admin/update/commits/route.ts +35 -0
  44. package/apps/agents-server/src/app/api/admin/update/route.ts +10 -2
  45. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +5 -0
  46. package/apps/agents-server/src/app/api/internal/user-chat-jobs/run/route.ts +14 -2
  47. package/apps/agents-server/src/app/api/v1/agents/[agentId]/route.ts +4 -0
  48. package/apps/agents-server/src/app/api/v1/agents/route.ts +2 -0
  49. package/apps/agents-server/src/app/dashboard/page.tsx +11 -16
  50. package/apps/agents-server/src/app/recycle-bin/actions.ts +3 -0
  51. package/apps/agents-server/src/app/swagger/SwaggerApiKeysPanel.tsx +4 -1
  52. package/apps/agents-server/src/app/system/user-memory/UserMemoryClient.tsx +4 -2
  53. package/apps/agents-server/src/app/system/user-wallet/UserWalletClient.tsx +3 -0
  54. package/apps/agents-server/src/app/system/user-wallet/UserWalletRecordsTable.tsx +5 -1
  55. package/apps/agents-server/src/app/system/utilities/mocked-chats/MockedChatsEditorClient.tsx +3 -0
  56. package/apps/agents-server/src/app/system/utilities/mocked-chats/MockedChatsEditorSidebar.tsx +7 -8
  57. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -2
  58. package/apps/agents-server/src/components/UsersList/UsersList.tsx +4 -2
  59. package/apps/agents-server/src/database/migrations/2026-06-2600-agent-directory-performance-indexes.sql +14 -0
  60. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +92 -1
  61. package/apps/agents-server/src/tools/createAgentProgressTools.ts +8 -6
  62. package/apps/agents-server/src/utils/localization/formatServerLanguageHumanReadableDate.ts +70 -0
  63. package/apps/agents-server/src/utils/localization/getRequestServerLanguage.ts +32 -0
  64. package/apps/agents-server/src/utils/userChat/createRunUserChatJobExecutionContext.ts +17 -0
  65. package/apps/agents-server/src/utils/userChat/createRunUserChatJobPersistenceController.ts +0 -21
  66. package/apps/agents-server/src/utils/userChat/listUserChats.ts +5 -1
  67. package/apps/agents-server/src/utils/userChat/persistUserChatJobProgressCard.ts +31 -0
  68. package/apps/agents-server/src/utils/userChat/retryUserChatJob.ts +0 -4
  69. package/apps/agents-server/src/utils/userChat/runUserChatJob.ts +29 -0
  70. package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +2 -2
  71. package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +99 -401
  72. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +625 -51
  73. package/esm/index.es.js +252 -41
  74. package/esm/index.es.js.map +1 -1
  75. package/esm/scripts/run-codex-prompts/common/buildCoderRunProgressSnapshot.d.ts +5 -1
  76. package/esm/scripts/run-codex-prompts/common/cliProgressDisplay.d.ts +7 -0
  77. package/esm/scripts/run-codex-prompts/common/coderRunEstimateCache.d.ts +31 -0
  78. package/esm/scripts/run-codex-prompts/common/progressFormatting.d.ts +2 -0
  79. package/esm/scripts/run-codex-prompts/runners/claude-code/ClaudeCodeRunner.d.ts +3 -1
  80. package/esm/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +7 -0
  81. package/esm/src/book-3.0/cliAgentEnv.d.ts +1 -1
  82. package/esm/src/cli/cli-commands/coder/ThinkingLevel.d.ts +1 -1
  83. package/esm/src/version.d.ts +1 -1
  84. package/package.json +1 -1
  85. package/src/book-3.0/cliAgentEnv.ts +1 -1
  86. package/src/cli/cli-commands/agents-server/startAgentsServer.ts +1 -1
  87. package/src/cli/cli-commands/coder/ThinkingLevel.ts +3 -1
  88. package/src/other/templates/getTemplatesPipelineCollection.ts +911 -741
  89. package/src/version.ts +2 -2
  90. package/src/versions.txt +2 -0
  91. package/umd/index.umd.js +254 -44
  92. package/umd/index.umd.js.map +1 -1
  93. package/umd/scripts/run-codex-prompts/common/buildCoderRunProgressSnapshot.d.ts +5 -1
  94. package/umd/scripts/run-codex-prompts/common/cliProgressDisplay.d.ts +7 -0
  95. package/umd/scripts/run-codex-prompts/common/coderRunEstimateCache.d.ts +31 -0
  96. package/umd/scripts/run-codex-prompts/common/progressFormatting.d.ts +2 -0
  97. package/umd/scripts/run-codex-prompts/runners/claude-code/ClaudeCodeRunner.d.ts +3 -1
  98. package/umd/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +7 -0
  99. package/umd/src/book-3.0/cliAgentEnv.d.ts +1 -1
  100. package/umd/src/cli/cli-commands/coder/ThinkingLevel.d.ts +1 -1
  101. package/umd/src/version.d.ts +1 -1
@@ -1,9 +1,25 @@
1
1
  'use client';
2
2
 
3
- import { CheckCircle2, Download, Loader2, RefreshCcw, Rocket, Server, TriangleAlert } from 'lucide-react';
3
+ import {
4
+ CheckCircle2,
5
+ ChevronDown,
6
+ ChevronUp,
7
+ Download,
8
+ Loader2,
9
+ RefreshCcw,
10
+ Rocket,
11
+ Server,
12
+ Settings2,
13
+ TriangleAlert,
14
+ } from 'lucide-react';
4
15
  import { useCallback, useEffect, useMemo, useState } from 'react';
5
16
  import { AdminXtermTerminal } from '../../../components/AdminTerminal/AdminXtermTerminal';
6
17
  import { Card } from '../../../components/Homepage/Card';
18
+ import { useServerLanguage } from '../../../components/ServerLanguage/ServerLanguageProvider';
19
+ import type { ServerLanguageCode } from '../../../languages/ServerLanguageRegistry';
20
+ import { createServerLanguageMoment } from '../../../utils/localization/createServerLanguageMoment';
21
+ import { formatServerLanguageHumanReadableDate } from '../../../utils/localization/formatServerLanguageHumanReadableDate';
22
+ import { CustomCommitPicker, type CustomCommitPickerCandidate } from './CustomCommitPicker';
7
23
 
8
24
  /**
9
25
  * Browser-safe environment option returned by the update API.
@@ -13,6 +29,7 @@ type UpdateEnvironmentOption = {
13
29
  readonly branch: string;
14
30
  readonly label: string;
15
31
  readonly description: string;
32
+ readonly isCustom: boolean;
16
33
  };
17
34
 
18
35
  /**
@@ -46,9 +63,15 @@ type UpdateOverview = {
46
63
  readonly currentCommitSha: string | null;
47
64
  readonly currentCommitShortSha: string | null;
48
65
  readonly currentCommitMessage: string | null;
66
+ readonly currentCommitDate: string | null;
49
67
  readonly latestRemoteCommitSha: string | null;
50
68
  readonly latestRemoteCommitShortSha: string | null;
69
+ readonly latestRemoteCommitDate: string | null;
70
+ readonly commitsBehindCount: number | null;
51
71
  readonly isUpdateAvailable: boolean;
72
+ readonly originRepositoryUrl: string;
73
+ readonly isOriginRepositoryDefault: boolean;
74
+ readonly defaultOriginRepositoryUrl: string;
52
75
  readonly job: UpdateJobSnapshot;
53
76
  readonly error?: string;
54
77
  };
@@ -57,16 +80,18 @@ type UpdateOverview = {
57
80
  * Client UI for standalone VPS branch-aware self-updates.
58
81
  */
59
82
  export function UpdateClient() {
83
+ const { language } = useServerLanguage();
60
84
  const [overview, setOverview] = useState<UpdateOverview | null>(null);
61
85
  const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>('');
86
+ const [customRef, setCustomRef] = useState<string>('');
87
+ const [customCandidate, setCustomCandidate] = useState<CustomCommitPickerCandidate | null>(null);
88
+ const [originRepositoryUrlOverride, setOriginRepositoryUrlOverride] = useState<string>('');
89
+ const [isAdvancedExpanded, setIsAdvancedExpanded] = useState<boolean>(false);
62
90
  const [isLoading, setIsLoading] = useState(true);
63
91
  const [isStartingUpdate, setIsStartingUpdate] = useState(false);
64
92
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
65
93
  const [successMessage, setSuccessMessage] = useState<string | null>(null);
66
94
 
67
- /**
68
- * Loads the latest self-update overview from the server.
69
- */
70
95
  const loadOverview = useCallback(
71
96
  async (options?: { readonly isSilent?: boolean; readonly isRestartExpected?: boolean }): Promise<void> => {
72
97
  try {
@@ -86,6 +111,7 @@ export function UpdateClient() {
86
111
  setSelectedEnvironmentId(
87
112
  (currentSelectedEnvironmentId) => currentSelectedEnvironmentId || payload.currentEnvironment.id,
88
113
  );
114
+ setOriginRepositoryUrlOverride((currentValue) => currentValue || payload.originRepositoryUrl);
89
115
  if (payload.job.status === 'succeeded') {
90
116
  setSuccessMessage(getUpdateJobSuccessMessage(payload.job));
91
117
  } else if (payload.job.status === 'failed') {
@@ -131,16 +157,18 @@ export function UpdateClient() {
131
157
  null,
132
158
  [overview, selectedEnvironmentId],
133
159
  );
160
+ const isCustomEnvironmentSelected = selectedEnvironment?.isCustom === true;
134
161
  const isEnvironmentSwitchRequired =
135
162
  Boolean(selectedEnvironment) && selectedEnvironment?.id !== overview?.currentEnvironment.id;
136
163
  const isUpdateRunning = overview?.job.status === 'running';
137
164
  const updateTerminalId = `standalone-vps-update:${overview?.job.startedAt || overview?.job.finishedAt || overview?.job.status || 'loading'}`;
138
165
  const updateTerminalEmptyState =
139
166
  isLoading && !overview ? 'Loading update log...' : 'No persisted update log output yet.';
167
+ const isCustomRefRelease = isCustomEnvironmentSelected ? customCandidate?.isReleaseTag === true : true;
168
+ const isCustomRefMissing = isCustomEnvironmentSelected && !customRef.trim();
169
+ const isOriginOverrideChanged =
170
+ overview !== null && originRepositoryUrlOverride.trim() !== overview.originRepositoryUrl;
140
171
 
141
- /**
142
- * Starts one detached update run for the selected environment.
143
- */
144
172
  async function startUpdate(): Promise<void> {
145
173
  if (!selectedEnvironment) {
146
174
  return;
@@ -158,6 +186,8 @@ export function UpdateClient() {
158
186
  },
159
187
  body: JSON.stringify({
160
188
  environment: selectedEnvironment.id,
189
+ customRef: isCustomEnvironmentSelected ? customRef.trim() : null,
190
+ originRepositoryUrl: isOriginOverrideChanged ? originRepositoryUrlOverride.trim() : null,
161
191
  }),
162
192
  });
163
193
  const payload = (await response.json()) as UpdateOverview;
@@ -168,9 +198,11 @@ export function UpdateClient() {
168
198
 
169
199
  setOverview(payload);
170
200
  setSuccessMessage(
171
- isEnvironmentSwitchRequired
172
- ? `Switched to ${selectedEnvironment.label} and started the standalone VPS update.`
173
- : 'Standalone VPS update started.',
201
+ isCustomEnvironmentSelected
202
+ ? `Started the standalone VPS update to custom ref ${customRef.trim()}.`
203
+ : isEnvironmentSwitchRequired
204
+ ? `Switched to ${selectedEnvironment.label} and started the standalone VPS update.`
205
+ : 'Standalone VPS update started.',
174
206
  );
175
207
  } catch (error) {
176
208
  setErrorMessage(error instanceof Error ? error.message : 'Failed to start the update.');
@@ -185,8 +217,8 @@ export function UpdateClient() {
185
217
  <div>
186
218
  <h1 className="text-3xl font-light text-gray-900">Update</h1>
187
219
  <p className="mt-1 max-w-3xl text-sm text-gray-500">
188
- Switch the standalone VPS between Production, Live, Preview, and LTS, and update the managed
189
- Promptbook checkout with one click.
220
+ Switch the standalone VPS between Live, Preview, Production, LTS, or a custom ref, and update
221
+ the managed Promptbook checkout with one click.
190
222
  </p>
191
223
  </div>
192
224
 
@@ -220,77 +252,7 @@ export function UpdateClient() {
220
252
 
221
253
  <div className="grid gap-4 lg:grid-cols-[1.4fr_1fr]">
222
254
  <Card className="hover:border-gray-200 hover:shadow-md">
223
- <div className="space-y-4">
224
- <div className="flex items-start gap-3">
225
- <Server className="mt-0.5 h-5 w-5 text-blue-600" />
226
- <div>
227
- <h2 className="text-lg font-semibold text-slate-900">Current deployment</h2>
228
- <p className="mt-1 text-sm text-slate-500">
229
- The server currently tracks the <span className="font-medium">{overview?.currentEnvironment.label || 'Production'}</span>{' '}
230
- environment on branch <span className="font-mono">{overview?.currentEnvironment.branch || 'production'}</span>.
231
- </p>
232
- </div>
233
- </div>
234
-
235
- <dl className="grid gap-4 text-sm text-slate-600 sm:grid-cols-2">
236
- <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
237
- <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</dt>
238
- <dd className="mt-1 font-mono text-slate-900">
239
- {overview?.currentEnvironment.branch || 'production'}
240
- </dd>
241
- </div>
242
- <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
243
- <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
244
- Deployed commit
245
- </dt>
246
- <dd className="mt-1 font-mono text-slate-900">
247
- {overview?.currentCommitShortSha || 'Unknown'}
248
- </dd>
249
- </div>
250
- <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
251
- <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
252
- Latest remote commit
253
- </dt>
254
- <dd className="mt-1 font-mono text-slate-900">
255
- {overview?.latestRemoteCommitShortSha || 'Unknown'}
256
- </dd>
257
- </div>
258
- <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
259
- <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
260
- Update availability
261
- </dt>
262
- <dd className="mt-1 flex items-center gap-2 text-slate-900">
263
- {overview?.isUpdateAvailable ? (
264
- <>
265
- <TriangleAlert className="h-4 w-4 text-amber-500" />
266
- New commit available
267
- </>
268
- ) : (
269
- <>
270
- <CheckCircle2 className="h-4 w-4 text-emerald-600" />
271
- Up to date
272
- </>
273
- )}
274
- </dd>
275
- </div>
276
- </dl>
277
-
278
- {overview?.currentCommitMessage && (
279
- <div className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-600">
280
- <div className="text-xs font-semibold uppercase tracking-wide text-slate-500">
281
- Current commit message
282
- </div>
283
- <div className="mt-1 text-slate-900">{overview.currentCommitMessage}</div>
284
- </div>
285
- )}
286
-
287
- {overview?.repositoryDirectory && (
288
- <div className="text-xs text-slate-500">
289
- Managed repository:
290
- <span className="ml-2 font-mono text-slate-700">{overview.repositoryDirectory}</span>
291
- </div>
292
- )}
293
- </div>
255
+ <CurrentDeploymentCard overview={overview} language={language} />
294
256
  </Card>
295
257
 
296
258
  <Card className="hover:border-gray-200 hover:shadow-md">
@@ -328,13 +290,48 @@ export function UpdateClient() {
328
290
  </span>
329
291
  )}
330
292
  </div>
331
- <div className="mt-1 font-mono text-xs">{environment.branch}</div>
293
+ {environment.branch && (
294
+ <div className="mt-1 font-mono text-xs">{environment.branch}</div>
295
+ )}
332
296
  <div className="mt-2 text-sm opacity-80">{environment.description}</div>
333
297
  </button>
334
298
  );
335
299
  })}
336
300
  </div>
337
301
 
302
+ {isCustomEnvironmentSelected && overview?.isAvailable && (
303
+ <div className="space-y-3 rounded-xl border border-blue-200 bg-blue-50/40 p-4">
304
+ <CustomCommitPicker
305
+ language={language}
306
+ selectedRef={customRef}
307
+ onSelectRef={(nextRef, matchedCandidate) => {
308
+ setCustomRef(nextRef);
309
+ setCustomCandidate(matchedCandidate);
310
+ }}
311
+ isDisabled={isUpdateRunning || isStartingUpdate}
312
+ />
313
+
314
+ {customRef.trim() && !isCustomRefRelease && (
315
+ <div className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
316
+ <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0" />
317
+ <div>
318
+ <strong>Heads up:</strong> the selected ref is not a release tag, so it may
319
+ be unstable and was not validated by the regular release pipeline.
320
+ </div>
321
+ </div>
322
+ )}
323
+ </div>
324
+ )}
325
+
326
+ <AdvancedOriginRepositoryPanel
327
+ originRepositoryUrlOverride={originRepositoryUrlOverride}
328
+ onChange={setOriginRepositoryUrlOverride}
329
+ overview={overview}
330
+ isExpanded={isAdvancedExpanded}
331
+ onToggleExpanded={() => setIsAdvancedExpanded((current) => !current)}
332
+ isDisabled={isUpdateRunning || isStartingUpdate}
333
+ />
334
+
338
335
  <button
339
336
  type="button"
340
337
  onClick={() => void startUpdate()}
@@ -343,20 +340,28 @@ export function UpdateClient() {
343
340
  !selectedEnvironment ||
344
341
  isUpdateRunning ||
345
342
  isStartingUpdate ||
346
- (!isEnvironmentSwitchRequired && !overview?.isUpdateAvailable)
343
+ isCustomRefMissing ||
344
+ (!isCustomEnvironmentSelected &&
345
+ !isEnvironmentSwitchRequired &&
346
+ !overview?.isUpdateAvailable &&
347
+ !isOriginOverrideChanged)
347
348
  }
348
349
  className="inline-flex w-full items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-3 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
349
350
  >
350
351
  {isStartingUpdate || isUpdateRunning ? (
351
352
  <Loader2 className="h-4 w-4 animate-spin" />
353
+ ) : isCustomEnvironmentSelected ? (
354
+ <Rocket className="h-4 w-4" />
352
355
  ) : isEnvironmentSwitchRequired ? (
353
356
  <Rocket className="h-4 w-4" />
354
357
  ) : (
355
358
  <Download className="h-4 w-4" />
356
359
  )}
357
- {isEnvironmentSwitchRequired
358
- ? `Switch to ${selectedEnvironment?.label || 'selected environment'} and update`
359
- : 'Update to latest commit'}
360
+ {isCustomEnvironmentSelected
361
+ ? `Update to ${customRef.trim() || 'custom ref'}`
362
+ : isEnvironmentSwitchRequired
363
+ ? `Switch to ${selectedEnvironment?.label || 'selected environment'} and update`
364
+ : 'Update to latest commit'}
360
365
  </button>
361
366
  </div>
362
367
  </Card>
@@ -377,10 +382,10 @@ export function UpdateClient() {
377
382
  overview?.job.status === 'running'
378
383
  ? 'border-blue-200 bg-blue-50 text-blue-700'
379
384
  : overview?.job.status === 'failed'
380
- ? 'border-rose-200 bg-rose-50 text-rose-700'
381
- : overview?.job.status === 'succeeded'
382
- ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
383
- : 'border-slate-200 bg-slate-50 text-slate-500'
385
+ ? 'border-rose-200 bg-rose-50 text-rose-700'
386
+ : overview?.job.status === 'succeeded'
387
+ ? 'border-emerald-200 bg-emerald-50 text-emerald-700'
388
+ : 'border-slate-200 bg-slate-50 text-slate-500'
384
389
  }`}
385
390
  >
386
391
  {overview?.job.status || 'idle'}
@@ -390,7 +395,9 @@ export function UpdateClient() {
390
395
  <dl className="grid gap-4 text-sm text-slate-600 md:grid-cols-2 xl:grid-cols-4">
391
396
  <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
392
397
  <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Target</dt>
393
- <dd className="mt-1 text-slate-900">{overview?.job.targetEnvironment.label || 'Production'}</dd>
398
+ <dd className="mt-1 text-slate-900">
399
+ {overview?.job.targetEnvironment.label || 'Production'}
400
+ </dd>
394
401
  </div>
395
402
  <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
396
403
  <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Step</dt>
@@ -398,11 +405,15 @@ export function UpdateClient() {
398
405
  </div>
399
406
  <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
400
407
  <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Started</dt>
401
- <dd className="mt-1 text-slate-900">{formatTimestamp(overview?.job.startedAt)}</dd>
408
+ <dd className="mt-1 text-slate-900">
409
+ {formatHumanReadableTimestamp(overview?.job.startedAt, language)}
410
+ </dd>
402
411
  </div>
403
412
  <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
404
413
  <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Finished</dt>
405
- <dd className="mt-1 text-slate-900">{formatTimestamp(overview?.job.finishedAt)}</dd>
414
+ <dd className="mt-1 text-slate-900">
415
+ {formatHumanReadableTimestamp(overview?.job.finishedAt, language)}
416
+ </dd>
406
417
  </div>
407
418
  </dl>
408
419
 
@@ -436,6 +447,197 @@ export function UpdateClient() {
436
447
  );
437
448
  }
438
449
 
450
+ /**
451
+ * Current-deployment metrics card, including the new commits-behind and time-behind info.
452
+ *
453
+ * @private internal component of `<UpdateClient/>`
454
+ */
455
+ function CurrentDeploymentCard({
456
+ overview,
457
+ language,
458
+ }: {
459
+ readonly overview: UpdateOverview | null;
460
+ readonly language: ServerLanguageCode;
461
+ }) {
462
+ const currentCommitDateLabel = formatHumanReadableTimestamp(overview?.currentCommitDate ?? null, language);
463
+ const latestRemoteCommitDateLabel = formatHumanReadableTimestamp(
464
+ overview?.latestRemoteCommitDate ?? null,
465
+ language,
466
+ );
467
+ const driftLabel = useMemo(
468
+ () => buildDeploymentDriftLabel(overview, language),
469
+ [overview, language],
470
+ );
471
+
472
+ return (
473
+ <div className="space-y-4">
474
+ <div className="flex items-start gap-3">
475
+ <Server className="mt-0.5 h-5 w-5 text-blue-600" />
476
+ <div>
477
+ <h2 className="text-lg font-semibold text-slate-900">Current deployment</h2>
478
+ <p className="mt-1 text-sm text-slate-500">
479
+ The server currently tracks the{' '}
480
+ <span className="font-medium">
481
+ {overview?.currentEnvironment.label || 'Production'}
482
+ </span>{' '}
483
+ environment on branch{' '}
484
+ <span className="font-mono">{overview?.currentEnvironment.branch || 'production'}</span>.
485
+ </p>
486
+ </div>
487
+ </div>
488
+
489
+ <dl className="grid gap-4 text-sm text-slate-600 sm:grid-cols-2">
490
+ <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
491
+ <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</dt>
492
+ <dd className="mt-1 font-mono text-slate-900">
493
+ {overview?.currentEnvironment.branch || 'production'}
494
+ </dd>
495
+ </div>
496
+ <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
497
+ <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">Deployed commit</dt>
498
+ <dd className="mt-1 font-mono text-slate-900">{overview?.currentCommitShortSha || 'Unknown'}</dd>
499
+ <div className="mt-1 text-xs text-slate-500">{currentCommitDateLabel}</div>
500
+ </div>
501
+ <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
502
+ <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
503
+ Latest remote commit
504
+ </dt>
505
+ <dd className="mt-1 font-mono text-slate-900">
506
+ {overview?.latestRemoteCommitShortSha || 'Unknown'}
507
+ </dd>
508
+ <div className="mt-1 text-xs text-slate-500">{latestRemoteCommitDateLabel}</div>
509
+ </div>
510
+ <div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
511
+ <dt className="text-xs font-semibold uppercase tracking-wide text-slate-500">
512
+ Update availability
513
+ </dt>
514
+ <dd className="mt-1 flex items-center gap-2 text-slate-900">
515
+ {overview?.isUpdateAvailable ? (
516
+ <>
517
+ <TriangleAlert className="h-4 w-4 text-amber-500" />
518
+ {driftLabel}
519
+ </>
520
+ ) : (
521
+ <>
522
+ <CheckCircle2 className="h-4 w-4 text-emerald-600" />
523
+ Up to date
524
+ </>
525
+ )}
526
+ </dd>
527
+ </div>
528
+ </dl>
529
+
530
+ {overview?.currentCommitMessage && (
531
+ <div className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-600">
532
+ <div className="text-xs font-semibold uppercase tracking-wide text-slate-500">
533
+ Current commit message
534
+ </div>
535
+ <div className="mt-1 text-slate-900">{overview.currentCommitMessage}</div>
536
+ </div>
537
+ )}
538
+
539
+ {overview?.repositoryDirectory && (
540
+ <div className="text-xs text-slate-500">
541
+ Managed repository:
542
+ <span className="ml-2 font-mono text-slate-700">{overview.repositoryDirectory}</span>
543
+ </div>
544
+ )}
545
+ </div>
546
+ );
547
+ }
548
+
549
+ /**
550
+ * Collapsible advanced panel for overriding the upstream repository URL.
551
+ *
552
+ * @private internal component of `<UpdateClient/>`
553
+ */
554
+ function AdvancedOriginRepositoryPanel({
555
+ originRepositoryUrlOverride,
556
+ onChange,
557
+ overview,
558
+ isExpanded,
559
+ onToggleExpanded,
560
+ isDisabled,
561
+ }: {
562
+ readonly originRepositoryUrlOverride: string;
563
+ readonly onChange: (nextValue: string) => void;
564
+ readonly overview: UpdateOverview | null;
565
+ readonly isExpanded: boolean;
566
+ readonly onToggleExpanded: () => void;
567
+ readonly isDisabled: boolean;
568
+ }) {
569
+ return (
570
+ <div className="rounded-xl border border-slate-200 bg-white">
571
+ <button
572
+ type="button"
573
+ onClick={onToggleExpanded}
574
+ className="flex w-full items-center justify-between gap-3 px-4 py-3 text-sm font-semibold text-slate-700"
575
+ >
576
+ <span className="inline-flex items-center gap-2">
577
+ <Settings2 className="h-4 w-4" />
578
+ Advanced
579
+ </span>
580
+ {isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
581
+ </button>
582
+ {isExpanded && (
583
+ <div className="space-y-3 border-t border-slate-100 px-4 py-3 text-sm text-slate-600">
584
+ <label className="block">
585
+ <span className="text-xs font-semibold uppercase tracking-wide text-slate-500">
586
+ Upstream repository URL
587
+ </span>
588
+ <input
589
+ type="url"
590
+ value={originRepositoryUrlOverride}
591
+ onChange={(event) => onChange(event.target.value)}
592
+ disabled={isDisabled}
593
+ placeholder={overview?.defaultOriginRepositoryUrl || ''}
594
+ className="mt-1 w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
595
+ />
596
+ </label>
597
+ <p className="text-xs text-slate-500">
598
+ Defaults to <span className="font-mono">{overview?.defaultOriginRepositoryUrl}</span>. Only
599
+ change this if you self-host a fork of the original Promptbook repository — the new value is
600
+ persisted in the standalone VPS <code>.env</code>.
601
+ </p>
602
+ {overview && !overview.isOriginRepositoryDefault && (
603
+ <div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
604
+ <strong>Custom origin active:</strong> updates currently pull from{' '}
605
+ <span className="font-mono">{overview.originRepositoryUrl}</span>.
606
+ </div>
607
+ )}
608
+ </div>
609
+ )}
610
+ </div>
611
+ );
612
+ }
613
+
614
+ /**
615
+ * Builds the "behind by …" label combining commits-behind and time-behind info.
616
+ *
617
+ * @param overview - Current overview snapshot.
618
+ * @param language - Active UI language for moment localization.
619
+ * @returns Human-readable label or fallback text.
620
+ */
621
+ function buildDeploymentDriftLabel(overview: UpdateOverview | null, language: ServerLanguageCode): string {
622
+ if (!overview) {
623
+ return 'New commit available';
624
+ }
625
+
626
+ const commitsBehindLabel =
627
+ overview.commitsBehindCount !== null && overview.commitsBehindCount > 0
628
+ ? `${overview.commitsBehindCount} commit${overview.commitsBehindCount === 1 ? '' : 's'} behind`
629
+ : 'New commit available';
630
+
631
+ if (!overview.currentCommitDate || !overview.latestRemoteCommitDate) {
632
+ return commitsBehindLabel;
633
+ }
634
+
635
+ const currentMoment = createServerLanguageMoment(overview.currentCommitDate, language);
636
+ const latestMoment = createServerLanguageMoment(overview.latestRemoteCommitDate, language);
637
+ const timeBehindLabel = currentMoment.from(latestMoment, true);
638
+ return `${commitsBehindLabel} · ${timeBehindLabel} behind`;
639
+ }
640
+
439
641
  /**
440
642
  * Builds the success message for a finished standalone VPS update.
441
643
  *
@@ -457,20 +659,15 @@ function getUpdateJobFailureMessage(job: UpdateJobSnapshot): string {
457
659
  }
458
660
 
459
661
  /**
460
- * Formats optional timestamps for the status cards.
662
+ * Formats optional timestamps for the status cards using `moment.js` with the active UI language.
461
663
  *
462
664
  * @param value - ISO timestamp or `null`.
463
- * @returns Human-friendly timestamp or fallback text.
665
+ * @param language - Active UI language code.
666
+ * @returns Localized human-friendly timestamp or fallback text.
464
667
  */
465
- function formatTimestamp(value: string | null | undefined): string {
466
- if (!value) {
467
- return 'Not available';
468
- }
469
-
470
- const date = new Date(value);
471
- if (Number.isNaN(date.getTime())) {
472
- return value;
473
- }
474
-
475
- return `${date.toLocaleString()} (${value})`;
668
+ function formatHumanReadableTimestamp(value: string | null | undefined, language: ServerLanguageCode): string {
669
+ return formatServerLanguageHumanReadableDate(value, language, {
670
+ fallbackLabel: 'Not available',
671
+ isExactDateIncluded: true,
672
+ });
476
673
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useAgentNaming } from '@/src/components/AgentNaming/AgentNamingContext';
4
4
  import { Card } from '@/src/components/Homepage/Card';
5
+ import { useServerLanguage } from '@/src/components/ServerLanguage/ServerLanguageProvider';
5
6
  import {
6
7
  type UsageActorType,
7
8
  type UsageAgentOption,
@@ -35,7 +36,8 @@ type UsageClientProps = {
35
36
  */
36
37
  export function UsageClient(props: UsageClientProps) {
37
38
  const { formatText } = useAgentNaming();
38
- const usageClientState = useUsageClientState(props);
39
+ const { language } = useServerLanguage();
40
+ const usageClientState = useUsageClientState(props, language);
39
41
 
40
42
  return (
41
43
  <div className="container mx-auto px-4 py-8 space-y-6">
@@ -86,7 +88,7 @@ export function UsageClient(props: UsageClientProps) {
86
88
  ) : null}
87
89
 
88
90
  {!usageClientState.loading && usageClientState.data && (
89
- <UsageClientAnalyticsPanels data={usageClientState.data} metric={usageClientState.metric} />
91
+ <UsageClientAnalyticsPanels data={usageClientState.data} language={language} metric={usageClientState.metric} />
90
92
  )}
91
93
  </div>
92
94
  );
@@ -1,4 +1,5 @@
1
1
  import { Card } from '@/src/components/Homepage/Card';
2
+ import type { ServerLanguageCode } from '@/src/languages/ServerLanguageRegistry';
2
3
  import type { UsageAnalyticsResponse, UsageMetricMode } from '@/src/utils/usageAdmin';
3
4
  import type { ReactNode } from 'react';
4
5
  import { useMemo } from 'react';
@@ -10,6 +11,7 @@ import { UsageClientTimelineChart } from './UsageClientTimelineChart';
10
11
  */
11
12
  type UsageClientAnalyticsPanelsProps = {
12
13
  data: UsageAnalyticsResponse;
14
+ language: ServerLanguageCode;
13
15
  metric: UsageMetricMode;
14
16
  };
15
17
 
@@ -19,7 +21,7 @@ type UsageClientAnalyticsPanelsProps = {
19
21
  * @private function of UsageClient
20
22
  */
21
23
  export function UsageClientAnalyticsPanels(props: UsageClientAnalyticsPanelsProps) {
22
- const { data, metric } = props;
24
+ const { data, language, metric } = props;
23
25
 
24
26
  const summaryItems = useMemo(() => {
25
27
  const primaryMetricLabel = UsageClientFormatting.usageMetricLabel(metric);
@@ -62,7 +64,7 @@ export function UsageClientAnalyticsPanels(props: UsageClientAnalyticsPanelsProp
62
64
  {UsageClientFormatting.usageMetricDescription(metric)} in the selected timeframe.
63
65
  </p>
64
66
  </div>
65
- <UsageClientTimelineChart points={data.timeline} metric={metric} />
67
+ <UsageClientTimelineChart language={language} points={data.timeline} metric={metric} />
66
68
  </Card>
67
69
 
68
70
  <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
@@ -164,7 +166,7 @@ export function UsageClientAnalyticsPanels(props: UsageClientAnalyticsPanelsProp
164
166
  metric,
165
167
  UsageClientFormatting.resolveMetricValue(item, metric),
166
168
  ),
167
- UsageClientFormatting.formatDateTime(item.lastSeen),
169
+ UsageClientFormatting.formatDateTime(item.lastSeen, language),
168
170
  ])}
169
171
  />
170
172
  </Card>
@@ -182,7 +184,7 @@ export function UsageClientAnalyticsPanels(props: UsageClientAnalyticsPanelsProp
182
184
  metric,
183
185
  UsageClientFormatting.resolveMetricValue(item, metric),
184
186
  ),
185
- UsageClientFormatting.formatDateTime(item.lastSeen),
187
+ UsageClientFormatting.formatDateTime(item.lastSeen, language),
186
188
  ])}
187
189
  />
188
190
  </Card>