@olonjs/cli 3.0.89 → 3.0.90
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.
|
@@ -1641,7 +1641,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1641
1641
|
"@tiptap/extension-link": "^2.11.5",
|
|
1642
1642
|
"@tiptap/react": "^2.11.5",
|
|
1643
1643
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1644
|
-
"@olonjs/core": "^1.0.
|
|
1644
|
+
"@olonjs/core": "^1.0.78",
|
|
1645
1645
|
"clsx": "^2.1.1",
|
|
1646
1646
|
"lucide-react": "^0.474.0",
|
|
1647
1647
|
"react": "^19.0.0",
|
|
@@ -2612,6 +2612,9 @@ const TENANT_ID = 'alpha';
|
|
|
2612
2612
|
const filePages = getFilePages();
|
|
2613
2613
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2614
2614
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2615
|
+
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2616
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
2617
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
|
|
2615
2618
|
|
|
2616
2619
|
interface CloudSaveUiState {
|
|
2617
2620
|
isOpen: boolean;
|
|
@@ -2918,14 +2921,39 @@ function App() {
|
|
|
2918
2921
|
[isCloudMode, CLOUD_API_URL]
|
|
2919
2922
|
);
|
|
2920
2923
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
+
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
2925
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
2926
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
2927
|
+
for (const apiBase of apiBases) {
|
|
2928
|
+
try {
|
|
2929
|
+
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
2930
|
+
method: 'GET',
|
|
2931
|
+
headers: {
|
|
2932
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
2933
|
+
},
|
|
2934
|
+
});
|
|
2935
|
+
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
2936
|
+
if (!res.ok) continue;
|
|
2937
|
+
const items = Array.isArray(body.items) ? body.items : [];
|
|
2938
|
+
setAssetsManifest(items);
|
|
2939
|
+
return;
|
|
2940
|
+
} catch {
|
|
2941
|
+
// try next candidate
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
setAssetsManifest([]);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2924
2948
|
fetch('/api/list-assets')
|
|
2925
2949
|
.then((r) => (r.ok ? r.json() : []))
|
|
2926
2950
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
2927
2951
|
.catch(() => setAssetsManifest([]));
|
|
2928
|
-
}, []);
|
|
2952
|
+
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY, cloudApiCandidates]);
|
|
2953
|
+
|
|
2954
|
+
useEffect(() => {
|
|
2955
|
+
void loadAssetsManifest();
|
|
2956
|
+
}, [loadAssetsManifest]);
|
|
2929
2957
|
|
|
2930
2958
|
useEffect(() => {
|
|
2931
2959
|
return () => {
|
|
@@ -3278,14 +3306,60 @@ function App() {
|
|
|
3278
3306
|
assetsBaseUrl: '/assets',
|
|
3279
3307
|
assetsManifest,
|
|
3280
3308
|
async onAssetUpload(file: File): Promise<string> {
|
|
3281
|
-
// Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
|
|
3282
|
-
// For now, this works in Local Mode.
|
|
3283
3309
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
3310
|
+
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
3311
|
+
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
3312
|
+
}
|
|
3284
3313
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
3285
|
-
|
|
3314
|
+
|
|
3315
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
3316
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
3317
|
+
let lastError: Error | null = null;
|
|
3318
|
+
for (const apiBase of apiBases) {
|
|
3319
|
+
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
3320
|
+
try {
|
|
3321
|
+
const formData = new FormData();
|
|
3322
|
+
formData.append('file', file);
|
|
3323
|
+
formData.append('filename', file.name);
|
|
3324
|
+
const controller = new AbortController();
|
|
3325
|
+
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
3326
|
+
const res = await fetch(`${apiBase}/assets/upload`, {
|
|
3327
|
+
method: 'POST',
|
|
3328
|
+
headers: {
|
|
3329
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3330
|
+
'X-Correlation-Id': crypto.randomUUID(),
|
|
3331
|
+
},
|
|
3332
|
+
body: formData,
|
|
3333
|
+
signal: controller.signal,
|
|
3334
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
3335
|
+
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
3336
|
+
if (res.ok && typeof body.url === 'string') {
|
|
3337
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3338
|
+
return body.url;
|
|
3339
|
+
}
|
|
3340
|
+
lastError = new Error(body.error || body.code || `Cloud upload failed: ${res.status}`);
|
|
3341
|
+
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3342
|
+
await sleep(backoffDelayMs(attempt));
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
break;
|
|
3346
|
+
} catch (error: unknown) {
|
|
3347
|
+
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
3348
|
+
lastError = new Error(message);
|
|
3349
|
+
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3350
|
+
await sleep(backoffDelayMs(attempt));
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
throw lastError ?? new Error('Cloud upload failed.');
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3286
3360
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
3287
3361
|
const reader = new FileReader();
|
|
3288
|
-
reader.onload
|
|
3362
|
+
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
3289
3363
|
reader.onerror = () => reject(reader.error);
|
|
3290
3364
|
reader.readAsDataURL(file);
|
|
3291
3365
|
});
|
|
@@ -3295,10 +3369,10 @@ function App() {
|
|
|
3295
3369
|
headers: { 'Content-Type': 'application/json' },
|
|
3296
3370
|
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
3297
3371
|
});
|
|
3298
|
-
|
|
3299
3372
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
3300
3373
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
3301
3374
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
3375
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3302
3376
|
return body.url;
|
|
3303
3377
|
},
|
|
3304
3378
|
},
|
|
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
596
596
|
"@tiptap/extension-link": "^2.11.5",
|
|
597
597
|
"@tiptap/react": "^2.11.5",
|
|
598
598
|
"@tiptap/starter-kit": "^2.11.5",
|
|
599
|
-
"@olonjs/core": "^1.0.
|
|
599
|
+
"@olonjs/core": "^1.0.78",
|
|
600
600
|
"clsx": "^2.1.1",
|
|
601
601
|
"lucide-react": "^0.474.0",
|
|
602
602
|
"react": "^19.0.0",
|
|
@@ -957,6 +957,9 @@ const TENANT_ID = 'santamamma'; // 🌿 SantaMamma Agriturismo
|
|
|
957
957
|
const filePages = getFilePages();
|
|
958
958
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
959
959
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
960
|
+
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
961
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
962
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
|
|
960
963
|
|
|
961
964
|
interface CloudSaveUiState {
|
|
962
965
|
isOpen: boolean;
|
|
@@ -980,6 +983,30 @@ function stepProgress(doneSteps: StepId[]): number {
|
|
|
980
983
|
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
981
984
|
}
|
|
982
985
|
|
|
986
|
+
function normalizeApiBase(raw: string): string {
|
|
987
|
+
return raw.trim().replace(/\/+$/, '');
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function buildUploadEndpoint(raw: string): string {
|
|
991
|
+
const base = normalizeApiBase(raw);
|
|
992
|
+
const withApi = /\/api\/v1$/i.test(base) ? base : `${base}/api/v1`;
|
|
993
|
+
return `${withApi}/assets/upload`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function isRetryableStatus(status: number): boolean {
|
|
997
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function backoffDelayMs(attempt: number): number {
|
|
1001
|
+
const base = 250 * Math.pow(2, attempt);
|
|
1002
|
+
const jitter = Math.floor(Math.random() * 120);
|
|
1003
|
+
return base + jitter;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function sleep(ms: number): Promise<void> {
|
|
1007
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
983
1010
|
function App() {
|
|
984
1011
|
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
985
1012
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
@@ -988,12 +1015,36 @@ function App() {
|
|
|
988
1015
|
const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
|
|
989
1016
|
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
990
1017
|
|
|
991
|
-
|
|
1018
|
+
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
1019
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
1020
|
+
try {
|
|
1021
|
+
const res = await fetch(`${buildUploadEndpoint(CLOUD_API_URL).replace(/\/upload$/, '/list')}?limit=200`, {
|
|
1022
|
+
method: 'GET',
|
|
1023
|
+
headers: {
|
|
1024
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
1028
|
+
if (res.ok) {
|
|
1029
|
+
setAssetsManifest(Array.isArray(body.items) ? body.items : []);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
} catch {
|
|
1033
|
+
// fallback to empty
|
|
1034
|
+
}
|
|
1035
|
+
setAssetsManifest([]);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
992
1039
|
fetch('/api/list-assets')
|
|
993
1040
|
.then((r) => (r.ok ? r.json() : []))
|
|
994
1041
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
995
1042
|
.catch(() => setAssetsManifest([]));
|
|
996
|
-
}, []);
|
|
1043
|
+
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY]);
|
|
1044
|
+
|
|
1045
|
+
useEffect(() => {
|
|
1046
|
+
void loadAssetsManifest();
|
|
1047
|
+
}, [loadAssetsManifest]);
|
|
997
1048
|
|
|
998
1049
|
useEffect(() => {
|
|
999
1050
|
return () => { activeCloudSaveController.current?.abort(); };
|
|
@@ -1104,7 +1155,53 @@ function App() {
|
|
|
1104
1155
|
assetsManifest,
|
|
1105
1156
|
async onAssetUpload(file: File): Promise<string> {
|
|
1106
1157
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
1158
|
+
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
1159
|
+
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
1160
|
+
}
|
|
1107
1161
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
1162
|
+
|
|
1163
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
1164
|
+
let lastError: Error | null = null;
|
|
1165
|
+
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
1166
|
+
try {
|
|
1167
|
+
const formData = new FormData();
|
|
1168
|
+
formData.append('file', file);
|
|
1169
|
+
formData.append('filename', file.name);
|
|
1170
|
+
const controller = new AbortController();
|
|
1171
|
+
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
1172
|
+
const res = await fetch(buildUploadEndpoint(CLOUD_API_URL), {
|
|
1173
|
+
method: 'POST',
|
|
1174
|
+
headers: {
|
|
1175
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1176
|
+
'X-Correlation-Id': crypto.randomUUID(),
|
|
1177
|
+
},
|
|
1178
|
+
body: formData,
|
|
1179
|
+
signal: controller.signal,
|
|
1180
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
1181
|
+
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
1182
|
+
if (res.ok && typeof body.url === 'string') {
|
|
1183
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
1184
|
+
return body.url;
|
|
1185
|
+
}
|
|
1186
|
+
lastError = new Error(body.error || body.code || `Upload failed: ${res.status}`);
|
|
1187
|
+
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
1188
|
+
await sleep(backoffDelayMs(attempt));
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
break;
|
|
1192
|
+
} catch (error: unknown) {
|
|
1193
|
+
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
1194
|
+
lastError = new Error(message);
|
|
1195
|
+
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
1196
|
+
await sleep(backoffDelayMs(attempt));
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
throw lastError ?? new Error('Cloud upload failed.');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1108
1205
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
1109
1206
|
const reader = new FileReader();
|
|
1110
1207
|
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
@@ -1119,6 +1216,7 @@ function App() {
|
|
|
1119
1216
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
1120
1217
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
1121
1218
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
1219
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
1122
1220
|
return body.url;
|
|
1123
1221
|
},
|
|
1124
1222
|
},
|
|
@@ -1641,7 +1641,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1641
1641
|
"@tiptap/extension-link": "^2.11.5",
|
|
1642
1642
|
"@tiptap/react": "^2.11.5",
|
|
1643
1643
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1644
|
-
"@olonjs/core": "^1.0.
|
|
1644
|
+
"@olonjs/core": "^1.0.78",
|
|
1645
1645
|
"clsx": "^2.1.1",
|
|
1646
1646
|
"lucide-react": "^0.474.0",
|
|
1647
1647
|
"react": "^19.0.0",
|
|
@@ -2612,6 +2612,9 @@ const TENANT_ID = 'alpha';
|
|
|
2612
2612
|
const filePages = getFilePages();
|
|
2613
2613
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2614
2614
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2615
|
+
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2616
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
2617
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
|
|
2615
2618
|
|
|
2616
2619
|
interface CloudSaveUiState {
|
|
2617
2620
|
isOpen: boolean;
|
|
@@ -2918,14 +2921,39 @@ function App() {
|
|
|
2918
2921
|
[isCloudMode, CLOUD_API_URL]
|
|
2919
2922
|
);
|
|
2920
2923
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
+
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
2925
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
2926
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
2927
|
+
for (const apiBase of apiBases) {
|
|
2928
|
+
try {
|
|
2929
|
+
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
2930
|
+
method: 'GET',
|
|
2931
|
+
headers: {
|
|
2932
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
2933
|
+
},
|
|
2934
|
+
});
|
|
2935
|
+
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
2936
|
+
if (!res.ok) continue;
|
|
2937
|
+
const items = Array.isArray(body.items) ? body.items : [];
|
|
2938
|
+
setAssetsManifest(items);
|
|
2939
|
+
return;
|
|
2940
|
+
} catch {
|
|
2941
|
+
// try next candidate
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
setAssetsManifest([]);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2924
2948
|
fetch('/api/list-assets')
|
|
2925
2949
|
.then((r) => (r.ok ? r.json() : []))
|
|
2926
2950
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
2927
2951
|
.catch(() => setAssetsManifest([]));
|
|
2928
|
-
}, []);
|
|
2952
|
+
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY, cloudApiCandidates]);
|
|
2953
|
+
|
|
2954
|
+
useEffect(() => {
|
|
2955
|
+
void loadAssetsManifest();
|
|
2956
|
+
}, [loadAssetsManifest]);
|
|
2929
2957
|
|
|
2930
2958
|
useEffect(() => {
|
|
2931
2959
|
return () => {
|
|
@@ -3278,14 +3306,60 @@ function App() {
|
|
|
3278
3306
|
assetsBaseUrl: '/assets',
|
|
3279
3307
|
assetsManifest,
|
|
3280
3308
|
async onAssetUpload(file: File): Promise<string> {
|
|
3281
|
-
// Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
|
|
3282
|
-
// For now, this works in Local Mode.
|
|
3283
3309
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
3310
|
+
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
3311
|
+
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
3312
|
+
}
|
|
3284
3313
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
3285
|
-
|
|
3314
|
+
|
|
3315
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
3316
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
3317
|
+
let lastError: Error | null = null;
|
|
3318
|
+
for (const apiBase of apiBases) {
|
|
3319
|
+
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
3320
|
+
try {
|
|
3321
|
+
const formData = new FormData();
|
|
3322
|
+
formData.append('file', file);
|
|
3323
|
+
formData.append('filename', file.name);
|
|
3324
|
+
const controller = new AbortController();
|
|
3325
|
+
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
3326
|
+
const res = await fetch(`${apiBase}/assets/upload`, {
|
|
3327
|
+
method: 'POST',
|
|
3328
|
+
headers: {
|
|
3329
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3330
|
+
'X-Correlation-Id': crypto.randomUUID(),
|
|
3331
|
+
},
|
|
3332
|
+
body: formData,
|
|
3333
|
+
signal: controller.signal,
|
|
3334
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
3335
|
+
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
3336
|
+
if (res.ok && typeof body.url === 'string') {
|
|
3337
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3338
|
+
return body.url;
|
|
3339
|
+
}
|
|
3340
|
+
lastError = new Error(body.error || body.code || `Cloud upload failed: ${res.status}`);
|
|
3341
|
+
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3342
|
+
await sleep(backoffDelayMs(attempt));
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
break;
|
|
3346
|
+
} catch (error: unknown) {
|
|
3347
|
+
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
3348
|
+
lastError = new Error(message);
|
|
3349
|
+
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3350
|
+
await sleep(backoffDelayMs(attempt));
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
throw lastError ?? new Error('Cloud upload failed.');
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3286
3360
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
3287
3361
|
const reader = new FileReader();
|
|
3288
|
-
reader.onload
|
|
3362
|
+
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
3289
3363
|
reader.onerror = () => reject(reader.error);
|
|
3290
3364
|
reader.readAsDataURL(file);
|
|
3291
3365
|
});
|
|
@@ -3295,10 +3369,10 @@ function App() {
|
|
|
3295
3369
|
headers: { 'Content-Type': 'application/json' },
|
|
3296
3370
|
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
3297
3371
|
});
|
|
3298
|
-
|
|
3299
3372
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
3300
3373
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
3301
3374
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
3375
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3302
3376
|
return body.url;
|
|
3303
3377
|
},
|
|
3304
3378
|
},
|