@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.
- package/assets/src_tenant_alpha.sh +5566 -1723
- package/assets/templates/agritourism/src_tenant.sh +101 -3
- package/assets/templates/alpha/src_tenant.sh +5566 -1723
- package/package.json +1 -1
|
@@ -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.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
|
-
|
|
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
|
},
|