@promptbook/cli 0.112.0-135 → 0.112.0-137
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/README.md +20 -0
- package/apps/agents-server/README.md +0 -1
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +246 -15
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +237 -4
- package/apps/agents-server/src/app/api/admin/update/log/route.ts +42 -0
- package/apps/agents-server/src/app/api/agents/export/route.ts +41 -0
- package/apps/agents-server/src/app/api/agents/import/route.ts +168 -0
- package/apps/agents-server/src/app/api/federated-agents/route.ts +1 -1
- package/apps/agents-server/src/app/api/internal/agent-runner-limits/route.ts +2 -1
- package/apps/agents-server/src/app/api/internal/user-chat-jobs/run/route.ts +2 -1
- package/apps/agents-server/src/app/api/metadata/export/route.ts +34 -0
- package/apps/agents-server/src/app/api/metadata/import/route.ts +46 -0
- package/apps/agents-server/src/app/layout.tsx +1 -1
- package/apps/agents-server/src/components/Homepage/AgentsList.tsx +31 -1
- package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +60 -1
- package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +523 -0
- package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +16 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +0 -21
- package/apps/agents-server/src/database/seedCoreAgents.ts +235 -0
- package/apps/agents-server/src/database/seedDefaultAgents.ts +32 -1
- package/apps/agents-server/src/search/createDefaultServerSearchProviders/createFederatedAgentsSearchProvider.ts +1 -1
- package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +2 -9
- package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +68 -0
- package/apps/agents-server/src/utils/agentsTransfer/importAgentsFromFiles.ts +852 -0
- package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +15 -2
- package/apps/agents-server/src/utils/defaultAgents/loadDefaultAgentBooks.ts +46 -4
- package/apps/agents-server/src/utils/getFederatedServers.ts +3 -74
- package/apps/agents-server/src/utils/getWellKnownAgentUrl.ts +10 -4
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +1 -1
- package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +426 -0
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +2 -5
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerCoreAgents.ts +162 -0
- package/apps/agents-server/src/utils/session.ts +2 -1
- package/apps/agents-server/src/utils/vpsSelfUpdate.ts +110 -0
- package/esm/index.es.js +1 -1
- package/esm/index.es.js.map +1 -1
- package/esm/src/_packages/core.index.d.ts +0 -2
- package/esm/src/utils/isTimingSafeEqualString.d.ts +25 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/servers.ts +1 -16
- package/src/_packages/core.index.ts +0 -2
- package/src/commitments/_common/teamInternalAgentAccess.ts +2 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +789 -716
- package/src/utils/isTimingSafeEqualString.ts +43 -0
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +1 -1
- package/umd/index.umd.js.map +1 -1
- package/umd/src/_packages/core.index.d.ts +0 -2
- package/umd/src/utils/isTimingSafeEqualString.d.ts +25 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/defaultFederatedAgents/DefaultFederatedAgentsSyncOptions.ts +0 -9
- package/apps/agents-server/src/utils/defaultFederatedAgents/ensureDefaultFederatedAgentExists.ts +0 -277
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchCoreOrganizationPayload.ts +0 -39
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchFederatedAgentBook.ts +0 -43
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchWithDefaultFederatedAgentTimeout.ts +0 -28
- package/apps/agents-server/src/utils/defaultFederatedAgents/getDefaultFederatedAgentSyncPool.ts +0 -38
- package/apps/agents-server/src/utils/defaultFederatedAgents/loadActiveLocalAgentIdsByNormalizedName.ts +0 -41
- package/apps/agents-server/src/utils/defaultFederatedAgents/loadDefaultFederatedAgentSyncMetadata.ts +0 -76
- package/apps/agents-server/src/utils/defaultFederatedAgents/quoteIdentifier.ts +0 -11
- package/apps/agents-server/src/utils/defaultFederatedAgents/scheduleDefaultFederatedAgentsSync.ts +0 -88
- package/apps/agents-server/src/utils/defaultFederatedAgents/selectDefaultFederatedAgentsFromOrganizationPayload.ts +0 -181
- package/apps/agents-server/src/utils/defaultFederatedAgents/synchronizeDefaultFederatedAgents.ts +0 -77
package/README.md
CHANGED
|
@@ -27,6 +27,26 @@ Create persistent AI agents that turn your company's scattered knowledge into ac
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
### Standalone VPS
|
|
31
|
+
|
|
32
|
+
Run the standalone VPS installer only on a fresh server. Interactive mode asks for the installation values:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sudo curl -fsSL https://raw.githubusercontent.com/webgptorg/promptbook/refs/heads/main/other/vps/install.sh | bash
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Non-interactive mode takes defaults from command-line options and skips initial code-runner CLI setup, which can be configured later from the UI or SSH:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sudo curl -fsSL https://raw.githubusercontent.com/webgptorg/promptbook/refs/heads/main/other/vps/install.sh | bash -s -- \
|
|
42
|
+
--non-interactive \
|
|
43
|
+
--yes-i-understand-that-script-should-be-run-on-fresh-server \
|
|
44
|
+
--domain s24.ptbk.io \
|
|
45
|
+
--openai-api-key sk-proj-abcdef \
|
|
46
|
+
--sentry-dsn https://abc@def.ingest.de.sentry.io/123 \
|
|
47
|
+
--admin-password xxx
|
|
48
|
+
```
|
|
49
|
+
|
|
30
50
|
|
|
31
51
|
|
|
32
52
|
<blockquote style="color: #ff8811">
|
|
@@ -57,4 +57,3 @@ When creating new Agents server, search across the repository for [☁]
|
|
|
57
57
|
- [☁] If using `USE PROJECT` auto-auth, configure [GitHub App integration](./GITHUB_APP.md)
|
|
58
58
|
- [☁] Add the server to [the list of our servers](https://docs.google.com/spreadsheets/d/1X26iMQqubsxftqD1EJNSlzPYFS94QjCFPXyKdHHDeVs/edit?gid=848307752#gid=848307752)
|
|
59
59
|
- [☁] Run migration script _(run new instance)_
|
|
60
|
-
- [☁] For testing servers, look at the server and change the `CORE_SERVER` to `https://core-test.ptbk.io/`
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DownloadIcon,
|
|
5
|
+
FileTextIcon,
|
|
6
|
+
HashIcon,
|
|
7
|
+
ImageIcon,
|
|
8
|
+
ListIcon,
|
|
9
|
+
ShieldIcon,
|
|
10
|
+
ToggleLeftIcon,
|
|
11
|
+
TypeIcon,
|
|
12
|
+
UploadIcon,
|
|
13
|
+
} from 'lucide-react';
|
|
4
14
|
import Link from 'next/link';
|
|
5
15
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
6
16
|
import { showConfirm } from '../../../components/AsyncDialogs/asyncDialogs';
|
|
7
17
|
import { useFileUploadAvailability } from '../../../components/FileUploadAvailability/FileUploadAvailabilityContext';
|
|
18
|
+
import { getDeprecatedLimitMetadataDefinition, type DeprecatedLimitMetadataDefinition } from '../../../constants/serverLimits';
|
|
8
19
|
import { getMetadataDefinition, metadataDefaults, type MetadataDefinition } from '../../../database/metadataDefaults';
|
|
9
20
|
import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
|
|
21
|
+
import { downloadBlob, parseFilenameFromContentDisposition } from '../../../utils/download/browserFileDownload';
|
|
10
22
|
import { normalizeUploadFilename } from '../../../utils/normalization/normalizeUploadFilename';
|
|
11
23
|
import { buildDefaultUserFileUploadPath, uploadFileToServer } from '../../../utils/upload/uploadFileToServer';
|
|
12
|
-
import { getDeprecatedLimitMetadataDefinition, type DeprecatedLimitMetadataDefinition } from '../../../constants/serverLimits';
|
|
13
24
|
|
|
14
25
|
/**
|
|
15
26
|
* Type describing metadata entry.
|
|
@@ -26,6 +37,47 @@ type MetadataEntry = {
|
|
|
26
37
|
deprecatedLimitMetadata?: DeprecatedLimitMetadataDefinition | null;
|
|
27
38
|
};
|
|
28
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Endpoint serving standalone metadata JSON exports.
|
|
42
|
+
*
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
const METADATA_EXPORT_ENDPOINT = '/api/metadata/export';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Endpoint accepting standalone metadata JSON imports.
|
|
49
|
+
*
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
const METADATA_IMPORT_ENDPOINT = '/api/metadata/import';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fallback filename used when the export route omits a `Content-Disposition` filename.
|
|
56
|
+
*
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
const DEFAULT_METADATA_EXPORT_FILENAME = 'promptbook-agents-server.metadata.json';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Minimal API error payload returned by metadata transfer routes.
|
|
63
|
+
*
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
type MetadataTransferErrorPayload = {
|
|
67
|
+
error?: string;
|
|
68
|
+
message?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Import summary returned by the metadata import route.
|
|
73
|
+
*
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
type MetadataImportSummary = {
|
|
77
|
+
importedCount?: number;
|
|
78
|
+
resetCount?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
29
81
|
/**
|
|
30
82
|
* Shared class names for single-line metadata inputs.
|
|
31
83
|
*
|
|
@@ -70,6 +122,30 @@ const createEmptyFormState = (): MetadataFormState => ({
|
|
|
70
122
|
note: '',
|
|
71
123
|
});
|
|
72
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Reads a user-facing error message from a failed metadata transfer response.
|
|
127
|
+
*
|
|
128
|
+
* @param response - Failed HTTP response.
|
|
129
|
+
* @param fallbackMessage - Message used when the response body is not useful JSON.
|
|
130
|
+
* @returns Error message for the admin UI.
|
|
131
|
+
*
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
async function resolveMetadataTransferErrorMessage(response: Response, fallbackMessage: string): Promise<string> {
|
|
135
|
+
try {
|
|
136
|
+
const payload = (await response.json()) as MetadataTransferErrorPayload;
|
|
137
|
+
const message = payload.message || payload.error;
|
|
138
|
+
|
|
139
|
+
if (message && message.trim().length > 0) {
|
|
140
|
+
return message.trim();
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// Keep fallback when the failed response is not JSON.
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return fallbackMessage;
|
|
147
|
+
}
|
|
148
|
+
|
|
73
149
|
/**
|
|
74
150
|
* Validates whether the provided string is an IPv4, IPv6, or CIDR range.
|
|
75
151
|
*
|
|
@@ -185,7 +261,7 @@ const MetadataValueField = ({
|
|
|
185
261
|
disabled={isUploading || !isUploadAvailable}
|
|
186
262
|
className="bg-gray-100 text-gray-700 px-3 py-2 rounded-md border border-gray-300 hover:bg-gray-200 flex items-center space-x-2 min-w-max"
|
|
187
263
|
>
|
|
188
|
-
<
|
|
264
|
+
<UploadIcon className="w-4 h-4" />
|
|
189
265
|
<span>{isUploading ? 'Uploading...' : 'Upload Image'}</span>
|
|
190
266
|
</button>
|
|
191
267
|
</div>
|
|
@@ -345,13 +421,18 @@ export function MetadataClient() {
|
|
|
345
421
|
const [metadata, setMetadata] = useState<MetadataEntry[]>([]);
|
|
346
422
|
const [loading, setLoading] = useState(true);
|
|
347
423
|
const [error, setError] = useState<string | null>(null);
|
|
424
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
348
425
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
|
349
426
|
const [isAddUploading, setIsAddUploading] = useState(false);
|
|
350
427
|
const [isEditUploading, setIsEditUploading] = useState(false);
|
|
428
|
+
const [isExportingMetadata, setIsExportingMetadata] = useState(false);
|
|
429
|
+
const [isImportingMetadata, setIsImportingMetadata] = useState(false);
|
|
351
430
|
const addFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
352
431
|
const editFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
432
|
+
const importFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
353
433
|
const [addFormState, setAddFormState] = useState<MetadataFormState>(createEmptyFormState);
|
|
354
434
|
const [editingFormState, setEditingFormState] = useState<MetadataFormState>(createEmptyFormState);
|
|
435
|
+
const isMetadataTransferBusy = isExportingMetadata || isImportingMetadata;
|
|
355
436
|
|
|
356
437
|
const fetchMetadata = async () => {
|
|
357
438
|
try {
|
|
@@ -365,8 +446,8 @@ export function MetadataClient() {
|
|
|
365
446
|
const mergedData = mergeMetadataWithDefaults(data);
|
|
366
447
|
|
|
367
448
|
setMetadata(mergedData);
|
|
368
|
-
} catch (
|
|
369
|
-
setError(
|
|
449
|
+
} catch (error) {
|
|
450
|
+
setError(error instanceof Error ? error.message : 'An error occurred');
|
|
370
451
|
} finally {
|
|
371
452
|
setLoading(false);
|
|
372
453
|
}
|
|
@@ -394,16 +475,123 @@ export function MetadataClient() {
|
|
|
394
475
|
}
|
|
395
476
|
};
|
|
396
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Downloads the current metadata configuration as standalone JSON.
|
|
480
|
+
*
|
|
481
|
+
* @private
|
|
482
|
+
*/
|
|
483
|
+
const handleExportMetadata = async () => {
|
|
484
|
+
if (isMetadataTransferBusy) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
setError(null);
|
|
489
|
+
setSuccessMessage(null);
|
|
490
|
+
setIsExportingMetadata(true);
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const response = await fetch(METADATA_EXPORT_ENDPOINT, { method: 'GET' });
|
|
494
|
+
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
setError(await resolveMetadataTransferErrorMessage(response, 'Failed to export metadata.'));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const filename =
|
|
501
|
+
parseFilenameFromContentDisposition(response.headers.get('Content-Disposition')) ||
|
|
502
|
+
DEFAULT_METADATA_EXPORT_FILENAME;
|
|
503
|
+
|
|
504
|
+
downloadBlob(await response.blob(), filename);
|
|
505
|
+
setSuccessMessage('Metadata export downloaded.');
|
|
506
|
+
} catch (error) {
|
|
507
|
+
setError(error instanceof Error ? error.message : 'Metadata export failed.');
|
|
508
|
+
} finally {
|
|
509
|
+
setIsExportingMetadata(false);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Opens the native file picker for metadata imports.
|
|
515
|
+
*
|
|
516
|
+
* @private
|
|
517
|
+
*/
|
|
518
|
+
const handleImportMetadataClick = () => {
|
|
519
|
+
if (isMetadataTransferBusy) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
importFileInputRef.current?.click();
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Imports selected standalone metadata JSON and refreshes the table.
|
|
528
|
+
*
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
const handleImportMetadataFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
532
|
+
const file = event.target.files?.[0];
|
|
533
|
+
if (!file || isMetadataTransferBusy) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const isConfirmed = await showConfirm({
|
|
538
|
+
title: 'Import metadata',
|
|
539
|
+
message: 'Import metadata from this JSON file? Existing built-in metadata not present in the file will be reset to defaults.',
|
|
540
|
+
confirmLabel: 'Import metadata',
|
|
541
|
+
cancelLabel: 'Cancel',
|
|
542
|
+
}).catch(() => false);
|
|
543
|
+
|
|
544
|
+
if (!isConfirmed) {
|
|
545
|
+
if (importFileInputRef.current) {
|
|
546
|
+
importFileInputRef.current.value = '';
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
setError(null);
|
|
552
|
+
setSuccessMessage(null);
|
|
553
|
+
setIsImportingMetadata(true);
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const response = await fetch(METADATA_IMPORT_ENDPOINT, {
|
|
557
|
+
method: 'POST',
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: await file.text(),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
if (!response.ok) {
|
|
563
|
+
setError(await resolveMetadataTransferErrorMessage(response, 'Failed to import metadata.'));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const importSummary = (await response.json()) as MetadataImportSummary;
|
|
568
|
+
await fetchMetadata();
|
|
569
|
+
setSuccessMessage(
|
|
570
|
+
`Imported ${importSummary.importedCount ?? 0} metadata entries and reset ${
|
|
571
|
+
importSummary.resetCount ?? 0
|
|
572
|
+
} built-in defaults.`,
|
|
573
|
+
);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
setError(error instanceof Error ? error.message : 'Metadata import failed.');
|
|
576
|
+
} finally {
|
|
577
|
+
setIsImportingMetadata(false);
|
|
578
|
+
if (importFileInputRef.current) {
|
|
579
|
+
importFileInputRef.current.value = '';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
397
584
|
const handleAddSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
398
585
|
e.preventDefault();
|
|
399
586
|
setError(null);
|
|
587
|
+
setSuccessMessage(null);
|
|
400
588
|
|
|
401
589
|
try {
|
|
402
590
|
await saveMetadata('POST', addFormState);
|
|
403
591
|
setAddFormState(createEmptyFormState());
|
|
404
592
|
fetchMetadata();
|
|
405
|
-
} catch (
|
|
406
|
-
setError(
|
|
593
|
+
} catch (error) {
|
|
594
|
+
setError(error instanceof Error ? error.message : 'An error occurred');
|
|
407
595
|
}
|
|
408
596
|
};
|
|
409
597
|
|
|
@@ -411,6 +599,7 @@ export function MetadataClient() {
|
|
|
411
599
|
e.preventDefault();
|
|
412
600
|
if (editingKey === null) return;
|
|
413
601
|
setError(null);
|
|
602
|
+
setSuccessMessage(null);
|
|
414
603
|
|
|
415
604
|
try {
|
|
416
605
|
const isPersistedEntry = metadata.some((entry) => entry.key === editingKey && !entry.isDefault);
|
|
@@ -418,8 +607,8 @@ export function MetadataClient() {
|
|
|
418
607
|
await saveMetadata(method, editingFormState);
|
|
419
608
|
handleCancel();
|
|
420
609
|
fetchMetadata();
|
|
421
|
-
} catch (
|
|
422
|
-
setError(
|
|
610
|
+
} catch (error) {
|
|
611
|
+
setError(error instanceof Error ? error.message : 'An error occurred');
|
|
423
612
|
}
|
|
424
613
|
};
|
|
425
614
|
|
|
@@ -433,15 +622,16 @@ export function MetadataClient() {
|
|
|
433
622
|
};
|
|
434
623
|
|
|
435
624
|
const handleDelete = async (key: string) => {
|
|
436
|
-
const
|
|
625
|
+
const isConfirmed = await showConfirm({
|
|
437
626
|
title: 'Delete metadata',
|
|
438
627
|
message: 'Are you sure you want to delete this metadata?',
|
|
439
628
|
confirmLabel: 'Delete metadata',
|
|
440
629
|
cancelLabel: 'Cancel',
|
|
441
630
|
}).catch(() => false);
|
|
442
|
-
if (!
|
|
631
|
+
if (!isConfirmed) return;
|
|
443
632
|
|
|
444
633
|
try {
|
|
634
|
+
setSuccessMessage(null);
|
|
445
635
|
const response = await fetch(`/api/metadata?key=${encodeURIComponent(key)}`, {
|
|
446
636
|
method: 'DELETE',
|
|
447
637
|
});
|
|
@@ -452,8 +642,8 @@ export function MetadataClient() {
|
|
|
452
642
|
}
|
|
453
643
|
|
|
454
644
|
fetchMetadata();
|
|
455
|
-
} catch (
|
|
456
|
-
setError(
|
|
645
|
+
} catch (error) {
|
|
646
|
+
setError(error instanceof Error ? error.message : 'An error occurred');
|
|
457
647
|
}
|
|
458
648
|
};
|
|
459
649
|
|
|
@@ -549,8 +739,8 @@ export function MetadataClient() {
|
|
|
549
739
|
|
|
550
740
|
const shortFileUrl = fileUrl.split(LONG_URL).join(SHORT_URL);
|
|
551
741
|
setFormState((prev) => ({ ...prev, value: shortFileUrl }));
|
|
552
|
-
} catch (
|
|
553
|
-
setError(
|
|
742
|
+
} catch (error) {
|
|
743
|
+
setError(error instanceof Error ? error.message : 'Failed to upload image');
|
|
554
744
|
} finally {
|
|
555
745
|
setUploading(false);
|
|
556
746
|
if (fileInputRef.current) {
|
|
@@ -586,6 +776,47 @@ export function MetadataClient() {
|
|
|
586
776
|
{error && (
|
|
587
777
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">{error}</div>
|
|
588
778
|
)}
|
|
779
|
+
{successMessage && (
|
|
780
|
+
<div className="bg-green-100 border border-green-300 text-green-800 px-4 py-3 rounded mb-6">
|
|
781
|
+
{successMessage}
|
|
782
|
+
</div>
|
|
783
|
+
)}
|
|
784
|
+
|
|
785
|
+
<div className="bg-white shadow rounded-lg p-6">
|
|
786
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
787
|
+
<div>
|
|
788
|
+
<h2 className="text-xl font-semibold">Import / Export Metadata</h2>
|
|
789
|
+
</div>
|
|
790
|
+
<div className="flex flex-wrap gap-3">
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
onClick={() => void handleExportMetadata()}
|
|
794
|
+
disabled={isMetadataTransferBusy}
|
|
795
|
+
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
796
|
+
>
|
|
797
|
+
<DownloadIcon className="h-4 w-4" />
|
|
798
|
+
<span>{isExportingMetadata ? 'Exporting...' : 'Export Metadata'}</span>
|
|
799
|
+
</button>
|
|
800
|
+
<input
|
|
801
|
+
type="file"
|
|
802
|
+
accept="application/json,.json"
|
|
803
|
+
className="hidden"
|
|
804
|
+
ref={importFileInputRef}
|
|
805
|
+
onChange={(event) => void handleImportMetadataFileChange(event)}
|
|
806
|
+
disabled={isMetadataTransferBusy}
|
|
807
|
+
/>
|
|
808
|
+
<button
|
|
809
|
+
type="button"
|
|
810
|
+
onClick={handleImportMetadataClick}
|
|
811
|
+
disabled={isMetadataTransferBusy}
|
|
812
|
+
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-gray-100 px-4 py-2 text-sm font-medium text-gray-800 transition-colors hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-60"
|
|
813
|
+
>
|
|
814
|
+
<UploadIcon className="h-4 w-4" />
|
|
815
|
+
<span>{isImportingMetadata ? 'Importing...' : 'Import Metadata'}</span>
|
|
816
|
+
</button>
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
589
820
|
|
|
590
821
|
<div className="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-sm text-amber-800">
|
|
591
822
|
Deprecated limit-backed metadata stays visible here for backward compatibility. Update operational
|