@olonjs/cli 3.0.89 → 3.0.91

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.
@@ -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.77",
599
+ "@olonjs/core": "^1.0.79",
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
- useEffect(() => {
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
  },