@promptbook/cli 0.112.0-110 → 0.112.0-112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +16 -0
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +31 -5
- package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +7 -5
- package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts +4 -86
- package/apps/agents-server/src/app/api/admin/servers/route.ts +5 -0
- package/apps/agents-server/src/app/api/metadata/route.ts +4 -0
- package/apps/agents-server/src/components/ApplicationErrorPage/ApplicationErrorPage.tsx +118 -12
- package/apps/agents-server/src/database/customJavascript.ts +62 -1
- package/apps/agents-server/src/database/customStylesheet.ts +60 -1
- package/apps/agents-server/src/database/getMetadata.ts +84 -3
- package/apps/agents-server/src/instrumentation.ts +3 -0
- package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -62
- package/apps/agents-server/src/utils/errorReporting/applicationErrorHandling.ts +45 -0
- package/apps/agents-server/src/utils/errorReporting/refreshApplicationDocument.ts +10 -0
- package/apps/agents-server/src/utils/errorReporting/registerServerErrorSentryLogging.ts +331 -0
- package/apps/agents-server/src/utils/errorReporting/sendApplicationErrorReportToSentry.ts +8 -153
- package/apps/agents-server/src/utils/errorReporting/sentryStore.ts +177 -0
- package/apps/agents-server/src/utils/importAgent.ts +1 -57
- package/apps/agents-server/src/utils/importAgentWithFallback.ts +0 -10
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +3 -1
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/normalizeCreateServerInput.ts +6 -0
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerDefaultAgents.ts +7 -3
- package/apps/agents-server/src/utils/serverManagement/createManagedServer.ts +5 -0
- package/apps/agents-server/src/utils/userChat/listUserChats.ts +109 -0
- package/apps/agents-server/src/utils/userChat.ts +0 -1
- package/esm/index.es.js +39 -13
- package/esm/index.es.js.map +1 -1
- package/esm/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +2 -1
- package/esm/src/cli/cli-commands/agents-server/startAgentsServer.test.d.ts +1 -0
- package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/cli-commands/agents-server/startAgentsServer.ts +23 -2
- package/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.ts +91 -14
- package/src/other/templates/getTemplatesPipelineCollection.ts +801 -652
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +39 -13
- package/umd/index.umd.js.map +1 -1
- package/umd/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +2 -1
- package/umd/src/cli/cli-commands/agents-server/startAgentsServer.test.d.ts +1 -0
- package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/userChat/getUserChatRevision.ts +0 -145
|
@@ -203,6 +203,22 @@ function CreateServerForm(props: {
|
|
|
203
203
|
) : null}
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
|
+
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
|
207
|
+
<label htmlFor="create-server-default-agents" className="flex items-start gap-3">
|
|
208
|
+
<input
|
|
209
|
+
id="create-server-default-agents"
|
|
210
|
+
type="checkbox"
|
|
211
|
+
checked={wizardState.isDefaultAgentsInstalled}
|
|
212
|
+
onChange={(event) => updateWizardField('isDefaultAgentsInstalled', event.target.checked)}
|
|
213
|
+
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
214
|
+
/>
|
|
215
|
+
<span>
|
|
216
|
+
<span className="block font-semibold text-gray-900">Install default agents</span>
|
|
217
|
+
<span className="mt-1 block">Create bundled starter agents from agents/default.</span>
|
|
218
|
+
</span>
|
|
219
|
+
</label>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
206
222
|
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600">
|
|
207
223
|
<p className="font-semibold text-gray-900">Admin user exists</p>
|
|
208
224
|
<p className="mt-1">
|
|
@@ -121,6 +121,11 @@ export type CreateServerWizardState = {
|
|
|
121
121
|
*/
|
|
122
122
|
additionalUsers: WizardUser[];
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Whether bundled default agents should be installed during server creation.
|
|
126
|
+
*/
|
|
127
|
+
isDefaultAgentsInstalled: boolean;
|
|
128
|
+
|
|
124
129
|
/**
|
|
125
130
|
* Initial metadata values.
|
|
126
131
|
*/
|
|
@@ -155,7 +160,10 @@ export type CreateServerWizardError = {
|
|
|
155
160
|
* @private function of <ServersClient/>
|
|
156
161
|
*/
|
|
157
162
|
export type UpdateCreateServerWizardField = <
|
|
158
|
-
TFieldName extends keyof Pick<
|
|
163
|
+
TFieldName extends keyof Pick<
|
|
164
|
+
CreateServerWizardState,
|
|
165
|
+
'name' | 'domain' | 'iconUrl' | 'isDefaultAgentsInstalled'
|
|
166
|
+
>,
|
|
159
167
|
>(
|
|
160
168
|
fieldName: TFieldName,
|
|
161
169
|
value: CreateServerWizardState[TFieldName],
|
|
@@ -262,6 +270,7 @@ function createInitialWizardState(): CreateServerWizardState {
|
|
|
262
270
|
password: '',
|
|
263
271
|
},
|
|
264
272
|
additionalUsers: [],
|
|
273
|
+
isDefaultAgentsInstalled: true,
|
|
265
274
|
initialSettings: {
|
|
266
275
|
language: 'en',
|
|
267
276
|
homepageMessage: '',
|
|
@@ -345,7 +354,8 @@ function hasCreateServerWizardChanges(wizardState: CreateServerWizardState): boo
|
|
|
345
354
|
wizardState.name !== initialWizardState.name ||
|
|
346
355
|
wizardState.identifier !== initialWizardState.identifier ||
|
|
347
356
|
wizardState.domain !== initialWizardState.domain ||
|
|
348
|
-
wizardState.iconUrl !== initialWizardState.iconUrl
|
|
357
|
+
wizardState.iconUrl !== initialWizardState.iconUrl ||
|
|
358
|
+
wizardState.isDefaultAgentsInstalled !== initialWizardState.isDefaultAgentsInstalled
|
|
349
359
|
);
|
|
350
360
|
}
|
|
351
361
|
|
|
@@ -402,16 +412,32 @@ export function useCreateServerWizard(options: UseCreateServerWizardOptions): Us
|
|
|
402
412
|
const updateWizardField = useCallback<UpdateCreateServerWizardField>((fieldName, value) => {
|
|
403
413
|
setWizardState((previous) => {
|
|
404
414
|
if (fieldName === 'name') {
|
|
415
|
+
const name = value as CreateServerWizardState['name'];
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
...previous,
|
|
419
|
+
name,
|
|
420
|
+
identifier: createServerIdentifierFromName(name),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (fieldName === 'domain') {
|
|
425
|
+
return {
|
|
426
|
+
...previous,
|
|
427
|
+
domain: value as CreateServerWizardState['domain'],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (fieldName === 'iconUrl') {
|
|
405
432
|
return {
|
|
406
433
|
...previous,
|
|
407
|
-
|
|
408
|
-
identifier: createServerIdentifierFromName(value),
|
|
434
|
+
iconUrl: value as CreateServerWizardState['iconUrl'],
|
|
409
435
|
};
|
|
410
436
|
}
|
|
411
437
|
|
|
412
438
|
return {
|
|
413
439
|
...previous,
|
|
414
|
-
|
|
440
|
+
isDefaultAgentsInstalled: value as CreateServerWizardState['isDefaultAgentsInstalled'],
|
|
415
441
|
};
|
|
416
442
|
});
|
|
417
443
|
}, []);
|
|
@@ -5,7 +5,7 @@ import { $provideAgentReferenceResolver } from '@/src/utils/agentReferenceResolv
|
|
|
5
5
|
import {
|
|
6
6
|
parseBookScopedAgentIdentifier,
|
|
7
7
|
} from '@/src/utils/agentReferenceResolver/bookScopedAgentReferences';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveCachedServerAgentContext } from '@/src/utils/cachedServerAgentRuntime';
|
|
9
9
|
import { cache } from 'react';
|
|
10
10
|
import type { AgentsServerDatabase } from '../../../database/schema';
|
|
11
11
|
import { $provideSupabaseForServer } from '../../../database/$provideSupabaseForServer';
|
|
@@ -91,10 +91,12 @@ export async function enforceCanonicalLocalAgentId(
|
|
|
91
91
|
* @returns Resolved agent profile with visibility.
|
|
92
92
|
*/
|
|
93
93
|
const getCachedAgentProfile = cache(async (agentName: string) => {
|
|
94
|
-
const collection = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
const [collection, { publicUrl }, baseAgentReferenceResolver] = await Promise.all([
|
|
95
|
+
$provideAgentCollectionForServer(),
|
|
96
|
+
$provideServer(),
|
|
97
|
+
$provideAgentReferenceResolver(),
|
|
98
|
+
]);
|
|
99
|
+
const resolvedAgentContext = await resolveCachedServerAgentContext({
|
|
98
100
|
collection,
|
|
99
101
|
agentIdentifier: agentName,
|
|
100
102
|
localServerUrl: publicUrl.href,
|
package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { CHAT_STREAM_KEEP_ALIVE_INTERVAL_MS } from '@/src/constants/streaming';
|
|
2
2
|
import { isPrivateModeEnabledFromRequest } from '@/src/utils/privateMode';
|
|
3
|
-
import {
|
|
4
|
-
createUserChatDetailPayload,
|
|
5
|
-
getUserChat,
|
|
6
|
-
getUserChatRevision,
|
|
7
|
-
isFrozenUserChatSource,
|
|
8
|
-
} from '@/src/utils/userChat';
|
|
3
|
+
import { createUserChatDetailPayload, getUserChat, isFrozenUserChatSource } from '@/src/utils/userChat';
|
|
9
4
|
import type { ChatMessage } from '@promptbook-local/types';
|
|
10
5
|
import { NextResponse } from 'next/server';
|
|
11
6
|
import { resolveUserChatScope } from '../../resolveUserChatScope';
|
|
@@ -37,11 +32,6 @@ type UserChatStreamFrame =
|
|
|
37
32
|
type: 'keepalive';
|
|
38
33
|
};
|
|
39
34
|
|
|
40
|
-
/**
|
|
41
|
-
* Lightweight revision shape returned by `getUserChatRevision`.
|
|
42
|
-
*/
|
|
43
|
-
type UserChatRevision = Awaited<ReturnType<typeof getUserChatRevision>>;
|
|
44
|
-
|
|
45
35
|
/**
|
|
46
36
|
* Streams canonical chat snapshots for one scoped user chat so multiple viewers can observe the same background turn.
|
|
47
37
|
*/
|
|
@@ -83,7 +73,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
83
73
|
async start(controller) {
|
|
84
74
|
let isStreamClosed = false;
|
|
85
75
|
let lastSnapshotSignature: string | null = null;
|
|
86
|
-
let lastRevisionSignature: string | null = null;
|
|
87
76
|
let keepAliveInterval: ReturnType<typeof setInterval> | null = null;
|
|
88
77
|
|
|
89
78
|
/**
|
|
@@ -149,24 +138,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
149
138
|
|
|
150
139
|
const payload = await createUserChatDetailPayload(currentChat);
|
|
151
140
|
const nextSignature = createUserChatDetailSignature(payload);
|
|
152
|
-
lastRevisionSignature = createUserChatRevisionSignature({
|
|
153
|
-
id: payload.chat.id,
|
|
154
|
-
updatedAt: payload.chat.updatedAt,
|
|
155
|
-
draftMessage: payload.draftMessage,
|
|
156
|
-
source: payload.chat.source,
|
|
157
|
-
activeJobs: payload.activeJobs.map((job) => ({
|
|
158
|
-
id: job.id,
|
|
159
|
-
status: job.status,
|
|
160
|
-
cancelRequestedAt: job.cancelRequestedAt,
|
|
161
|
-
})),
|
|
162
|
-
activeTimeouts: payload.activeTimeouts.map((timeout) => ({
|
|
163
|
-
id: timeout.id,
|
|
164
|
-
status: timeout.status,
|
|
165
|
-
message: timeout.message,
|
|
166
|
-
dueAt: timeout.dueAt,
|
|
167
|
-
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
168
|
-
})),
|
|
169
|
-
});
|
|
170
141
|
|
|
171
142
|
if (nextSignature !== lastSnapshotSignature) {
|
|
172
143
|
lastSnapshotSignature = nextSignature;
|
|
@@ -178,32 +149,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
178
149
|
return !isFrozenUserChatSource(payload.chat.source) && payload.activeJobs.length > 0;
|
|
179
150
|
};
|
|
180
151
|
|
|
181
|
-
/**
|
|
182
|
-
* Emits a full snapshot only when an idle chat revision changed.
|
|
183
|
-
*
|
|
184
|
-
* @private route helper
|
|
185
|
-
*/
|
|
186
|
-
const emitLatestSnapshotWhenRevisionChanged = async (): Promise<boolean> => {
|
|
187
|
-
const currentRevision = await getUserChatRevision({
|
|
188
|
-
userId: scopeResult.scope.userId,
|
|
189
|
-
viewerIsAdmin: scopeResult.scope.viewerIsAdmin,
|
|
190
|
-
agentPermanentId: scopeResult.scope.agentPermanentId,
|
|
191
|
-
chatId,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
if (!currentRevision) {
|
|
195
|
-
closeStream();
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const nextRevisionSignature = createUserChatRevisionSignature(currentRevision);
|
|
200
|
-
if (nextRevisionSignature === lastRevisionSignature) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return emitLatestSnapshot();
|
|
205
|
-
};
|
|
206
|
-
|
|
207
152
|
/**
|
|
208
153
|
* Tracks client disconnects so the polling loop can exit promptly.
|
|
209
154
|
*
|
|
@@ -220,11 +165,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
220
165
|
keepAliveInterval.unref?.();
|
|
221
166
|
|
|
222
167
|
try {
|
|
223
|
-
let
|
|
168
|
+
let hasActiveJobs = await emitLatestSnapshot();
|
|
224
169
|
|
|
225
170
|
while (!isStreamClosed && !request.signal.aborted) {
|
|
226
171
|
await waitForNextUserChatStreamPoll(
|
|
227
|
-
|
|
172
|
+
hasActiveJobs
|
|
228
173
|
? ACTIVE_USER_CHAT_STREAM_POLL_INTERVAL_MS
|
|
229
174
|
: IDLE_USER_CHAT_STREAM_POLL_INTERVAL_MS,
|
|
230
175
|
request.signal,
|
|
@@ -234,9 +179,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
234
179
|
break;
|
|
235
180
|
}
|
|
236
181
|
|
|
237
|
-
|
|
238
|
-
? await emitLatestSnapshot()
|
|
239
|
-
: await emitLatestSnapshotWhenRevisionChanged();
|
|
182
|
+
hasActiveJobs = await emitLatestSnapshot();
|
|
240
183
|
}
|
|
241
184
|
} catch (error) {
|
|
242
185
|
if (!isStreamClosed && !request.signal.aborted) {
|
|
@@ -267,30 +210,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
267
210
|
});
|
|
268
211
|
}
|
|
269
212
|
|
|
270
|
-
/**
|
|
271
|
-
* Builds a compact signature for idle stream revision checks.
|
|
272
|
-
*/
|
|
273
|
-
function createUserChatRevisionSignature(revision: NonNullable<UserChatRevision>): string {
|
|
274
|
-
return JSON.stringify({
|
|
275
|
-
id: revision.id,
|
|
276
|
-
updatedAt: revision.updatedAt,
|
|
277
|
-
draftMessage: revision.draftMessage || '',
|
|
278
|
-
source: revision.source,
|
|
279
|
-
activeJobs: revision.activeJobs.map((job) => ({
|
|
280
|
-
id: job.id,
|
|
281
|
-
status: job.status,
|
|
282
|
-
cancelRequestedAt: job.cancelRequestedAt,
|
|
283
|
-
})),
|
|
284
|
-
activeTimeouts: revision.activeTimeouts.map((timeout) => ({
|
|
285
|
-
id: timeout.id,
|
|
286
|
-
status: timeout.status,
|
|
287
|
-
message: timeout.message || '',
|
|
288
|
-
dueAt: timeout.dueAt,
|
|
289
|
-
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
290
|
-
})),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
213
|
/**
|
|
295
214
|
* Builds a stable signature for the user-visible parts of a canonical chat snapshot.
|
|
296
215
|
*/
|
|
@@ -308,7 +227,6 @@ function createUserChatDetailSignature(payload: Awaited<ReturnType<typeof create
|
|
|
308
227
|
activeTimeouts: payload.activeTimeouts.map((timeout) => ({
|
|
309
228
|
id: timeout.id,
|
|
310
229
|
status: timeout.status,
|
|
311
|
-
message: timeout.message || '',
|
|
312
230
|
dueAt: timeout.dueAt,
|
|
313
231
|
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
314
232
|
})),
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|
|
2
2
|
import { spaceTrim } from 'spacetrim';
|
|
3
3
|
import { DatabaseError } from '../../../../../../../src/errors/DatabaseError';
|
|
4
4
|
import { isAgentsServerSqliteMode } from '../../../../database/agentsServerDatabaseMode';
|
|
5
|
+
import { seedDefaultAgents } from '../../../../database/seedDefaultAgents';
|
|
5
6
|
import { resolveCurrentServerRegistryContext } from '../../../../utils/currentServerRegistryContext';
|
|
6
7
|
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
7
8
|
import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
|
|
@@ -97,6 +98,7 @@ export async function POST(request: Request) {
|
|
|
97
98
|
|
|
98
99
|
const body = withEnvironmentAdminUser((await request.json()) as CreateServerInput);
|
|
99
100
|
if (isAgentsServerSqliteMode()) {
|
|
101
|
+
const isDefaultAgentsInstalled = body.isDefaultAgentsInstalled !== false;
|
|
100
102
|
const normalizedDomain = normalizeServerDomain(body.domain);
|
|
101
103
|
if (!normalizedDomain) {
|
|
102
104
|
return NextResponse.json({ error: 'A valid domain is required.' }, { status: 400 });
|
|
@@ -114,6 +116,9 @@ export async function POST(request: Request) {
|
|
|
114
116
|
iconUrl: body.iconUrl,
|
|
115
117
|
});
|
|
116
118
|
}
|
|
119
|
+
if (isDefaultAgentsInstalled) {
|
|
120
|
+
await seedDefaultAgents({ tablePrefix });
|
|
121
|
+
}
|
|
117
122
|
|
|
118
123
|
return NextResponse.json(
|
|
119
124
|
{
|
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { keepUnused } from '../../../../../../src/utils/organization/keepUnused';
|
|
3
3
|
import { $getTableName } from '../../../database/$getTableName';
|
|
4
4
|
import { $provideSupabase } from '../../../database/$provideSupabase';
|
|
5
|
+
import { invalidateMetadataCache } from '../../../database/getMetadata';
|
|
5
6
|
import { validateMetadataValue } from '../../../database/metadataDefaults';
|
|
6
7
|
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
7
8
|
|
|
@@ -121,6 +122,7 @@ export async function POST(request: NextRequest) {
|
|
|
121
122
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
invalidateMetadataCache();
|
|
124
126
|
return NextResponse.json(data);
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -152,6 +154,7 @@ export async function PUT(request: NextRequest) {
|
|
|
152
154
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
invalidateMetadataCache();
|
|
155
158
|
return NextResponse.json(data);
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -179,5 +182,6 @@ export async function DELETE(request: NextRequest) {
|
|
|
179
182
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
invalidateMetadataCache();
|
|
182
186
|
return NextResponse.json({ success: true });
|
|
183
187
|
}
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
createApplicationErrorHeadline,
|
|
15
15
|
createApplicationErrorReportPayload,
|
|
16
16
|
describeApplicationError,
|
|
17
|
+
isApplicationErrorRecoverableByHardRefresh,
|
|
17
18
|
resolveApplicationErrorVariant,
|
|
18
19
|
} from '../../utils/errorReporting/applicationErrorHandling';
|
|
20
|
+
import { refreshApplicationDocument } from '../../utils/errorReporting/refreshApplicationDocument';
|
|
19
21
|
import { ErrorPageButtonAction, ErrorPageLinkAction } from '../ErrorPage/ErrorPage';
|
|
20
22
|
import { InternalServerErrorPage } from '../InternalServerErrorPage/InternalServerErrorPage';
|
|
21
23
|
|
|
@@ -27,7 +29,7 @@ import { InternalServerErrorPage } from '../InternalServerErrorPage/InternalServ
|
|
|
27
29
|
const troubleshootingSteps = [
|
|
28
30
|
{
|
|
29
31
|
title: 'Refresh the route',
|
|
30
|
-
detail: '
|
|
32
|
+
detail: 'Use the primary action so the page can retry with fresh cookies, network state, and build assets.',
|
|
31
33
|
},
|
|
32
34
|
{
|
|
33
35
|
title: 'Share the digest',
|
|
@@ -46,6 +48,20 @@ const troubleshootingSteps = [
|
|
|
46
48
|
*/
|
|
47
49
|
const REPORT_ACTION_FEEDBACK_DURATION_MS = 2500;
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Delay before one recoverable stale-asset error triggers an automatic full-document refresh.
|
|
53
|
+
*
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
const APPLICATION_ERROR_AUTO_REFRESH_DELAY_MS = 1500;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Prefix used for session-scoped stale-asset refresh guards.
|
|
60
|
+
*
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
const APPLICATION_ERROR_AUTO_REFRESH_STORAGE_KEY_PREFIX = 'promptbook.application-error-hard-refresh';
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Writes plain text to the user clipboard.
|
|
51
67
|
*
|
|
@@ -82,17 +98,25 @@ function downloadMarkdownReport(reportMarkdown: string, filename: string): void
|
|
|
82
98
|
URL.revokeObjectURL(reportUrl);
|
|
83
99
|
}
|
|
84
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Builds the session-storage key used to ensure one stale-asset error only auto-refreshes once per URL and digest.
|
|
103
|
+
*
|
|
104
|
+
* @param pageUrl - Browser URL where the error happened.
|
|
105
|
+
* @param digest - Deterministic digest rendered for the error.
|
|
106
|
+
* @returns Stable session-storage key for the current recoverable error.
|
|
107
|
+
*
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
function createApplicationErrorAutoRefreshStorageKey(pageUrl: string, digest: string): string {
|
|
111
|
+
return `${APPLICATION_ERROR_AUTO_REFRESH_STORAGE_KEY_PREFIX}:${pageUrl}:${digest}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
/**
|
|
86
115
|
* Props accepted by shared action controls used across error variants.
|
|
87
116
|
*
|
|
88
117
|
* @private
|
|
89
118
|
*/
|
|
90
119
|
type ApplicationErrorActionsProps = {
|
|
91
|
-
/**
|
|
92
|
-
* Callback that retries the failed route transition.
|
|
93
|
-
*/
|
|
94
|
-
reset: () => void;
|
|
95
|
-
|
|
96
120
|
/**
|
|
97
121
|
* Digest value displayed for operator correlation.
|
|
98
122
|
*/
|
|
@@ -107,6 +131,16 @@ type ApplicationErrorActionsProps = {
|
|
|
107
131
|
* Default filename used by the markdown save action.
|
|
108
132
|
*/
|
|
109
133
|
reportFilename: string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Primary action label rendered on the left-most action button.
|
|
137
|
+
*/
|
|
138
|
+
primaryActionLabel: string;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Callback used by the left-most primary action button.
|
|
142
|
+
*/
|
|
143
|
+
onPrimaryAction: () => void;
|
|
110
144
|
};
|
|
111
145
|
|
|
112
146
|
/**
|
|
@@ -117,10 +151,11 @@ type ApplicationErrorActionsProps = {
|
|
|
117
151
|
* @private
|
|
118
152
|
*/
|
|
119
153
|
function ApplicationErrorActions({
|
|
120
|
-
reset,
|
|
121
154
|
digest,
|
|
122
155
|
reportMarkdown,
|
|
123
156
|
reportFilename,
|
|
157
|
+
primaryActionLabel,
|
|
158
|
+
onPrimaryAction,
|
|
124
159
|
}: ApplicationErrorActionsProps) {
|
|
125
160
|
const [reportFeedback, setReportFeedback] = useState<string | null>(null);
|
|
126
161
|
const reportFeedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -179,9 +214,7 @@ function ApplicationErrorActions({
|
|
|
179
214
|
return (
|
|
180
215
|
<div className="flex flex-col items-center gap-3">
|
|
181
216
|
<div className="flex flex-wrap justify-center gap-3">
|
|
182
|
-
<ErrorPageButtonAction onClick={
|
|
183
|
-
Try again
|
|
184
|
-
</ErrorPageButtonAction>
|
|
217
|
+
<ErrorPageButtonAction onClick={onPrimaryAction}>{primaryActionLabel}</ErrorPageButtonAction>
|
|
185
218
|
<ErrorPageLinkAction href="/" tone="secondary">
|
|
186
219
|
Go to homepage
|
|
187
220
|
</ErrorPageLinkAction>
|
|
@@ -245,10 +278,25 @@ type ApplicationErrorViewProps = {
|
|
|
245
278
|
*/
|
|
246
279
|
size?: 'default' | 'wide';
|
|
247
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Primary action label rendered on the left-most action button.
|
|
283
|
+
*/
|
|
284
|
+
primaryActionLabel: string;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Callback used by the left-most primary action button.
|
|
288
|
+
*/
|
|
289
|
+
onPrimaryAction: () => void;
|
|
290
|
+
|
|
248
291
|
/**
|
|
249
292
|
* Optional supplementary content rendered beneath shared actions.
|
|
250
293
|
*/
|
|
251
294
|
supplementaryContent?: ReactNode;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Optional recovery note rendered between the actions and supplementary content.
|
|
298
|
+
*/
|
|
299
|
+
recoveryNotice?: ReactNode;
|
|
252
300
|
};
|
|
253
301
|
|
|
254
302
|
/**
|
|
@@ -264,18 +312,22 @@ function ApplicationErrorView({
|
|
|
264
312
|
digest,
|
|
265
313
|
reportMarkdown,
|
|
266
314
|
reportFilename,
|
|
267
|
-
reset,
|
|
268
315
|
size = 'default',
|
|
316
|
+
primaryActionLabel,
|
|
317
|
+
onPrimaryAction,
|
|
318
|
+
recoveryNotice,
|
|
269
319
|
supplementaryContent,
|
|
270
320
|
}: ApplicationErrorViewProps) {
|
|
271
321
|
return (
|
|
272
322
|
<InternalServerErrorPage headline={headline} description={description} size={size}>
|
|
273
323
|
<ApplicationErrorActions
|
|
274
|
-
reset={reset}
|
|
275
324
|
digest={digest}
|
|
276
325
|
reportMarkdown={reportMarkdown}
|
|
277
326
|
reportFilename={reportFilename}
|
|
327
|
+
primaryActionLabel={primaryActionLabel}
|
|
328
|
+
onPrimaryAction={onPrimaryAction}
|
|
278
329
|
/>
|
|
330
|
+
{recoveryNotice}
|
|
279
331
|
{supplementaryContent}
|
|
280
332
|
</InternalServerErrorPage>
|
|
281
333
|
);
|
|
@@ -374,9 +426,11 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
374
426
|
);
|
|
375
427
|
const digest = createApplicationErrorDigest(error);
|
|
376
428
|
const serverName = process.env.NEXT_PUBLIC_SERVER_NAME ?? DEFAULT_APPLICATION_ERROR_SERVER_NAME;
|
|
429
|
+
const isRecoverableByHardRefresh = isApplicationErrorRecoverableByHardRefresh(error);
|
|
377
430
|
const headline = createApplicationErrorHeadline(serverName);
|
|
378
431
|
const description = describeApplicationError(error, serverName);
|
|
379
432
|
const [pageUrl, setPageUrl] = useState<string | undefined>(undefined);
|
|
433
|
+
const [hasAutomaticRefreshAlreadyBeenAttempted, setHasAutomaticRefreshAlreadyBeenAttempted] = useState(false);
|
|
380
434
|
const reportPayload = useMemo(
|
|
381
435
|
() => createApplicationErrorReportPayload(error, digest, serverName, variant, pageUrl),
|
|
382
436
|
[digest, error, pageUrl, serverName, variant],
|
|
@@ -392,6 +446,35 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
392
446
|
setPageUrl(window.location.href);
|
|
393
447
|
}, []);
|
|
394
448
|
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!pageUrl || !isRecoverableByHardRefresh) {
|
|
451
|
+
setHasAutomaticRefreshAlreadyBeenAttempted(false);
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const storageKey = createApplicationErrorAutoRefreshStorageKey(pageUrl, digest);
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const hasStoredRefreshAttempt = window.sessionStorage.getItem(storageKey) === 'done';
|
|
459
|
+
if (hasStoredRefreshAttempt) {
|
|
460
|
+
setHasAutomaticRefreshAlreadyBeenAttempted(true);
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
window.sessionStorage.setItem(storageKey, 'done');
|
|
465
|
+
} catch {
|
|
466
|
+
// Some browser privacy modes may block session storage; the one-shot reload remains best-effort.
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const timeout = window.setTimeout(() => {
|
|
470
|
+
refreshApplicationDocument();
|
|
471
|
+
}, APPLICATION_ERROR_AUTO_REFRESH_DELAY_MS);
|
|
472
|
+
|
|
473
|
+
return () => {
|
|
474
|
+
window.clearTimeout(timeout);
|
|
475
|
+
};
|
|
476
|
+
}, [digest, isRecoverableByHardRefresh, pageUrl]);
|
|
477
|
+
|
|
395
478
|
useEffect(() => {
|
|
396
479
|
if (!pageUrl) {
|
|
397
480
|
return;
|
|
@@ -408,6 +491,23 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
408
491
|
});
|
|
409
492
|
}, [error, pageUrl, reportPayload]);
|
|
410
493
|
|
|
494
|
+
const primaryActionLabel = isRecoverableByHardRefresh ? 'Refresh now' : 'Try again';
|
|
495
|
+
const handlePrimaryAction = () => {
|
|
496
|
+
if (isRecoverableByHardRefresh) {
|
|
497
|
+
refreshApplicationDocument();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
reset();
|
|
502
|
+
};
|
|
503
|
+
const recoveryNotice = isRecoverableByHardRefresh ? (
|
|
504
|
+
<p className="mt-6 text-center text-sm text-gray-600">
|
|
505
|
+
{hasAutomaticRefreshAlreadyBeenAttempted
|
|
506
|
+
? 'Automatic refresh already ran once. If the newest assets still do not load, use "Refresh now" or share the digest with your administrator.'
|
|
507
|
+
: 'Refreshing this page automatically once so the latest application files can be loaded.'}
|
|
508
|
+
</p>
|
|
509
|
+
) : undefined;
|
|
510
|
+
|
|
411
511
|
if (variant === 'simple') {
|
|
412
512
|
return (
|
|
413
513
|
<SimpleApplicationErrorView
|
|
@@ -417,6 +517,9 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
417
517
|
reportMarkdown={reportMarkdown}
|
|
418
518
|
reportFilename={reportFilename}
|
|
419
519
|
reset={reset}
|
|
520
|
+
recoveryNotice={recoveryNotice}
|
|
521
|
+
primaryActionLabel={primaryActionLabel}
|
|
522
|
+
onPrimaryAction={handlePrimaryAction}
|
|
420
523
|
/>
|
|
421
524
|
);
|
|
422
525
|
}
|
|
@@ -429,6 +532,9 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
429
532
|
reportMarkdown={reportMarkdown}
|
|
430
533
|
reportFilename={reportFilename}
|
|
431
534
|
reset={reset}
|
|
535
|
+
recoveryNotice={recoveryNotice}
|
|
536
|
+
primaryActionLabel={primaryActionLabel}
|
|
537
|
+
onPrimaryAction={handlePrimaryAction}
|
|
432
538
|
/>
|
|
433
539
|
);
|
|
434
540
|
}
|
|
@@ -14,6 +14,13 @@ export const MAX_CUSTOM_JAVASCRIPT_LENGTH = 100_000;
|
|
|
14
14
|
*/
|
|
15
15
|
const CUSTOM_JAVASCRIPT_TABLE_BASENAME = 'CustomJavascript';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Process-level cache lifetime for custom JavaScript rows.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
const CUSTOM_JAVASCRIPT_CACHE_TTL_MS = 30_000;
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Stored `CustomJavascript` row shape.
|
|
19
26
|
*
|
|
@@ -90,6 +97,28 @@ type DynamicSupabaseClient = {
|
|
|
90
97
|
from: (tableName: string) => DynamicCustomJavascriptTableQuery;
|
|
91
98
|
};
|
|
92
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Cached custom JavaScript rows keyed by the resolved table name.
|
|
102
|
+
*
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
const cachedCustomJavascriptByTableName = new Map<
|
|
106
|
+
string,
|
|
107
|
+
{
|
|
108
|
+
readonly loadedAt: number;
|
|
109
|
+
readonly rowsPromise: Promise<CustomJavascriptRow[]>;
|
|
110
|
+
}
|
|
111
|
+
>();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clears process-level custom JavaScript cache after admin writes.
|
|
115
|
+
*
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
export function invalidateCustomJavascriptCache(): void {
|
|
119
|
+
cachedCustomJavascriptByTableName.clear();
|
|
120
|
+
}
|
|
121
|
+
|
|
93
122
|
/**
|
|
94
123
|
* Resolves the prefixed table name for `CustomJavascript`.
|
|
95
124
|
*
|
|
@@ -137,8 +166,37 @@ function getCustomJavascriptClient() {
|
|
|
137
166
|
*/
|
|
138
167
|
export async function getCustomJavascriptFiles(): Promise<CustomJavascriptRow[]> {
|
|
139
168
|
const table = await getCustomJavascriptTableName();
|
|
140
|
-
const
|
|
169
|
+
const cachedJavascript = cachedCustomJavascriptByTableName.get(table);
|
|
170
|
+
if (cachedJavascript && Date.now() - cachedJavascript.loadedAt < CUSTOM_JAVASCRIPT_CACHE_TTL_MS) {
|
|
171
|
+
return cachedJavascript.rowsPromise;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const rowsPromise = loadCustomJavascriptFilesFromDatabase(table);
|
|
175
|
+
cachedCustomJavascriptByTableName.set(table, {
|
|
176
|
+
loadedAt: Date.now(),
|
|
177
|
+
rowsPromise,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return await rowsPromise;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (cachedCustomJavascriptByTableName.get(table)?.rowsPromise === rowsPromise) {
|
|
184
|
+
cachedCustomJavascriptByTableName.delete(table);
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
141
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Reads custom JavaScript rows from the database.
|
|
192
|
+
*
|
|
193
|
+
* @param table - Resolved CustomJavascript table name.
|
|
194
|
+
* @returns Stored JavaScript rows.
|
|
195
|
+
*
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
async function loadCustomJavascriptFilesFromDatabase(table: string): Promise<CustomJavascriptRow[]> {
|
|
199
|
+
const supabase = getCustomJavascriptClient();
|
|
142
200
|
const { data, error } = await supabase.from(table).select('*').order('scope', { ascending: true });
|
|
143
201
|
|
|
144
202
|
if (error) {
|
|
@@ -234,6 +292,7 @@ export async function saveCustomJavascriptFile({
|
|
|
234
292
|
throw new Error(`Failed to save custom JavaScript: ${error.message || String(error)}`);
|
|
235
293
|
}
|
|
236
294
|
|
|
295
|
+
invalidateCustomJavascriptCache();
|
|
237
296
|
return data as CustomJavascriptRow;
|
|
238
297
|
}
|
|
239
298
|
|
|
@@ -255,4 +314,6 @@ export async function deleteCustomJavascriptFile(id: number): Promise<void> {
|
|
|
255
314
|
|
|
256
315
|
throw new Error(`Failed to delete custom JavaScript: ${error.message || String(error)}`);
|
|
257
316
|
}
|
|
317
|
+
|
|
318
|
+
invalidateCustomJavascriptCache();
|
|
258
319
|
}
|