@olonjs/cli 3.0.151 → 3.0.152
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 +154 -150
- package/assets/templates/alpha/src_tenant.sh +154 -150
- package/package.json +1 -1
|
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1821
|
-
"@olonjs/react": "^0.1.
|
|
1822
|
-
"@olonjs/studio": "^0.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.19",
|
|
1821
|
+
"@olonjs/react": "^0.1.2",
|
|
1822
|
+
"@olonjs/studio": "^0.1.2",
|
|
1823
1823
|
"class-variance-authority": "^0.7.1",
|
|
1824
1824
|
"clsx": "^2.1.1",
|
|
1825
1825
|
"lucide-react": "^0.474.0",
|
|
@@ -2952,7 +2952,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
2952
2952
|
* Bootstrap, persistence, and engine wiring — logic lives in lib/hooks.
|
|
2953
2953
|
*/
|
|
2954
2954
|
import { useEffect, useMemo } from 'react';
|
|
2955
|
-
import { JsonPagesEngine, OlonFormsContext } from '@olonjs/react';
|
|
2955
|
+
import { JsonPagesEngine, OlonFormsContext, createHotSaveHandler } from '@olonjs/react';
|
|
2956
2956
|
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
2957
2957
|
import { withBasePath } from '@olonjs/core';
|
|
2958
2958
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
@@ -2985,7 +2985,7 @@ import {
|
|
|
2985
2985
|
useInjectedTenantCss,
|
|
2986
2986
|
useTenantFontsReady,
|
|
2987
2987
|
} from '@/lib/tenantCss';
|
|
2988
|
-
import { APP_BASE_PATH,
|
|
2988
|
+
import { APP_BASE_PATH, TENANT_ID, cloudPolicy } from '@/lib/tenantEnv';
|
|
2989
2989
|
import { useAssetsManifest } from '@/lib/useAssetsManifest';
|
|
2990
2990
|
import { useCloudSave } from '@/lib/useCloudSave';
|
|
2991
2991
|
import { useTenantBootstrap } from '@/lib/useTenantBootstrap';
|
|
@@ -3010,16 +3010,40 @@ function App() {
|
|
|
3010
3010
|
themeConfigSeed,
|
|
3011
3011
|
});
|
|
3012
3012
|
useAdminStudioContent({
|
|
3013
|
-
enabled: bootstrap.
|
|
3013
|
+
enabled: bootstrap.bootSource === 'live',
|
|
3014
3014
|
apiCandidates: bootstrap.cloudApiCandidates,
|
|
3015
|
-
apiKey:
|
|
3015
|
+
apiKey: cloudPolicy.apiKey,
|
|
3016
3016
|
setPages: bootstrap.setPages,
|
|
3017
3017
|
setSiteConfig: bootstrap.setSiteConfig,
|
|
3018
|
-
|
|
3018
|
+
onSettled: bootstrap.markCloudContentReady,
|
|
3019
3019
|
});
|
|
3020
|
-
const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(
|
|
3020
|
+
const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(cloudPolicy.isCloudMode);
|
|
3021
3021
|
const { cloudSaveUi, runCloudSave, closeCloudDrawer, retryCloudSave } = useCloudSave();
|
|
3022
3022
|
|
|
3023
|
+
const hotSave = useMemo(
|
|
3024
|
+
() =>
|
|
3025
|
+
createHotSaveHandler({
|
|
3026
|
+
apiUrl: cloudPolicy.apiUrl,
|
|
3027
|
+
apiKey: cloudPolicy.apiKey,
|
|
3028
|
+
onSuccess: ({ state, slug, apiUrl, apiKey }) => {
|
|
3029
|
+
const keyFingerprint = cloudFingerprintFromUrl(apiUrl, apiKey);
|
|
3030
|
+
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3031
|
+
const existing = readCachedCloudContent(keyFingerprint);
|
|
3032
|
+
writeCachedCloudContent({
|
|
3033
|
+
keyFingerprint,
|
|
3034
|
+
savedAt: Date.now(),
|
|
3035
|
+
siteConfig: state.site ?? null,
|
|
3036
|
+
collections: state.collections,
|
|
3037
|
+
pages: {
|
|
3038
|
+
...(existing?.pages ?? {}),
|
|
3039
|
+
[normalizedSlug]: state.page,
|
|
3040
|
+
},
|
|
3041
|
+
});
|
|
3042
|
+
},
|
|
3043
|
+
}),
|
|
3044
|
+
[],
|
|
3045
|
+
);
|
|
3046
|
+
|
|
3023
3047
|
const tenantCssParts = useMemo(() => extractLeadingRemoteCssImports(tenantCss), []);
|
|
3024
3048
|
const resolvedTenantCss = useMemo(
|
|
3025
3049
|
() => [buildThemeFontVarsCss(bootstrap.themeConfig), tenantCssParts.rest].filter(Boolean).join('\n'),
|
|
@@ -3058,11 +3082,10 @@ function App() {
|
|
|
3058
3082
|
}, [canPaintVisitor, bootstrap.enginePages, bootstrap.siteConfig]);
|
|
3059
3083
|
|
|
3060
3084
|
const engineCollections =
|
|
3061
|
-
bootstrap.
|
|
3085
|
+
bootstrap.bootSource === 'live' || Object.keys(bootstrap.collections).length > 0
|
|
3062
3086
|
? bootstrap.collections
|
|
3063
3087
|
: fileCollections;
|
|
3064
3088
|
|
|
3065
|
-
// TOASK: why this if we have a menuConfig Object?
|
|
3066
3089
|
const engineRefDocuments = useMemo(
|
|
3067
3090
|
() => ({
|
|
3068
3091
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3071,7 +3094,6 @@ function App() {
|
|
|
3071
3094
|
}),
|
|
3072
3095
|
[bootstrap.menuConfig],
|
|
3073
3096
|
);
|
|
3074
|
-
// TOASK END
|
|
3075
3097
|
const config: JsonPagesConfig = {
|
|
3076
3098
|
tenantId: TENANT_ID,
|
|
3077
3099
|
basePath: APP_BASE_PATH,
|
|
@@ -3110,48 +3132,13 @@ function App() {
|
|
|
3110
3132
|
setCollections: bootstrap.setCollections,
|
|
3111
3133
|
});
|
|
3112
3134
|
},
|
|
3113
|
-
|
|
3114
|
-
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3115
|
-
throw new Error('Cloud mode is not configured for hot save.');
|
|
3116
|
-
}
|
|
3117
|
-
const apiBase = CLOUD_API_URL.replace(/\/$/, '');
|
|
3118
|
-
const res = await fetch(`${apiBase}/hotSave`, {
|
|
3119
|
-
method: 'POST',
|
|
3120
|
-
headers: {
|
|
3121
|
-
'Content-Type': 'application/json',
|
|
3122
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3123
|
-
},
|
|
3124
|
-
body: JSON.stringify({
|
|
3125
|
-
slug,
|
|
3126
|
-
page: state.page,
|
|
3127
|
-
siteConfig: state.site,
|
|
3128
|
-
collections: state.collections,
|
|
3129
|
-
}),
|
|
3130
|
-
});
|
|
3131
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
3132
|
-
if (!res.ok) {
|
|
3133
|
-
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
3134
|
-
}
|
|
3135
|
-
const keyFingerprint = cloudFingerprintFromUrl(CLOUD_API_URL, CLOUD_API_KEY);
|
|
3136
|
-
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3137
|
-
const existing = readCachedCloudContent(keyFingerprint);
|
|
3138
|
-
writeCachedCloudContent({
|
|
3139
|
-
keyFingerprint,
|
|
3140
|
-
savedAt: Date.now(),
|
|
3141
|
-
siteConfig: state.site ?? null,
|
|
3142
|
-
collections: state.collections,
|
|
3143
|
-
pages: {
|
|
3144
|
-
...(existing?.pages ?? {}),
|
|
3145
|
-
[normalizedSlug]: state.page,
|
|
3146
|
-
},
|
|
3147
|
-
});
|
|
3148
|
-
},
|
|
3135
|
+
hotSave,
|
|
3149
3136
|
async coldSave(state: ProjectState, slug: string): Promise<void> {
|
|
3150
3137
|
await runCloudSave({ state, slug }, true);
|
|
3151
3138
|
},
|
|
3152
|
-
showLocalSave:
|
|
3153
|
-
showHotSave:
|
|
3154
|
-
showColdSave:
|
|
3139
|
+
showLocalSave: cloudPolicy.showLocalSave,
|
|
3140
|
+
showHotSave: cloudPolicy.showHotSave,
|
|
3141
|
+
showColdSave: cloudPolicy.showColdSave,
|
|
3155
3142
|
},
|
|
3156
3143
|
assets: {
|
|
3157
3144
|
assetsBaseUrl: withBasePath('/assets', APP_BASE_PATH),
|
|
@@ -3159,9 +3146,9 @@ function App() {
|
|
|
3159
3146
|
onAssetUpload: (file) =>
|
|
3160
3147
|
uploadTenantAsset(file, {
|
|
3161
3148
|
basePath: APP_BASE_PATH,
|
|
3162
|
-
isCloudMode:
|
|
3163
|
-
cloudApiUrl:
|
|
3164
|
-
cloudApiKey:
|
|
3149
|
+
isCloudMode: cloudPolicy.isCloudMode,
|
|
3150
|
+
cloudApiUrl: cloudPolicy.apiUrl,
|
|
3151
|
+
cloudApiKey: cloudPolicy.apiKey,
|
|
3165
3152
|
apiBases: cloudApiCandidates,
|
|
3166
3153
|
onUploaded: loadAssetsManifest,
|
|
3167
3154
|
}),
|
|
@@ -3172,7 +3159,7 @@ function App() {
|
|
|
3172
3159
|
<ThemeProvider>
|
|
3173
3160
|
<OlonFormsContext.Provider value={formStates}>
|
|
3174
3161
|
<TenantBootstrapChrome
|
|
3175
|
-
isCloudMode={
|
|
3162
|
+
isCloudMode={cloudPolicy.isCloudMode}
|
|
3176
3163
|
showTopProgress={bootstrap.showTopProgress || (bootstrap.shouldRenderEngine && !fontsReady)}
|
|
3177
3164
|
contentMode={bootstrap.contentMode}
|
|
3178
3165
|
contentFallback={bootstrap.contentFallback}
|
|
@@ -4089,15 +4076,15 @@ echo "Creating src/components/form-demo/View.tsx..."
|
|
|
4089
4076
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
4090
4077
|
import { Icon } from '@/lib/IconResolver';
|
|
4091
4078
|
import { useFormState } from '@olonjs/react';
|
|
4079
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
4092
4080
|
import type { FormDemoData } from './types';
|
|
4093
4081
|
|
|
4094
4082
|
type FormDemoViewProps = {
|
|
4095
4083
|
data: FormDemoData;
|
|
4096
4084
|
};
|
|
4097
4085
|
|
|
4098
|
-
const
|
|
4099
|
-
|
|
4100
|
-
!import.meta.env.VITE_OLONJS_CLOUD_URL;
|
|
4086
|
+
const missingCloudUrl = !cloudPolicy.apiUrl;
|
|
4087
|
+
const hasCloudKey = Boolean(cloudPolicy.apiKey);
|
|
4101
4088
|
|
|
4102
4089
|
function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
4103
4090
|
const steps = [
|
|
@@ -4107,14 +4094,14 @@ function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
|
4107
4094
|
code: '"recipientEmail": "tu@esempio.it"',
|
|
4108
4095
|
},
|
|
4109
4096
|
{
|
|
4110
|
-
done: !
|
|
4111
|
-
label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
|
|
4112
|
-
code: '
|
|
4097
|
+
done: !missingCloudUrl,
|
|
4098
|
+
label: 'VITE_OLONJS_CLOUD_URL (o VITE_JSONPAGES_CLOUD_URL) nel file .env',
|
|
4099
|
+
code: 'VITE_OLONJS_CLOUD_URL=https://cloud.olonjs.io/api/v1',
|
|
4113
4100
|
},
|
|
4114
4101
|
{
|
|
4115
|
-
done:
|
|
4116
|
-
label: 'VITE_JSONPAGES_API_KEY nel file .env',
|
|
4117
|
-
code: '
|
|
4102
|
+
done: hasCloudKey,
|
|
4103
|
+
label: 'VITE_OLONJS_API_KEY (o VITE_JSONPAGES_API_KEY) nel file .env',
|
|
4104
|
+
code: 'VITE_OLONJS_API_KEY=sk-...',
|
|
4118
4105
|
},
|
|
4119
4106
|
];
|
|
4120
4107
|
|
|
@@ -11610,9 +11597,8 @@ echo "Creating src/lib/cloud/useAdminStudioContent.ts..."
|
|
|
11610
11597
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloud/useAdminStudioContent.ts"
|
|
11611
11598
|
import { useEffect, useRef } from 'react';
|
|
11612
11599
|
import type { Dispatch, SetStateAction } from 'react';
|
|
11613
|
-
import type { JsonPagesConfig } from '@olonjs/core';
|
|
11614
11600
|
import { applyLegacyCloudPayload, fetchLegacyCloudContentPayload } from '@/lib/cloud/cloudContentClient';
|
|
11615
|
-
import { cloudFingerprint, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11601
|
+
import { cloudFingerprint, readCachedPages, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11616
11602
|
import { isAdminPath, patchHistoryNavigation } from '@/lib/spp';
|
|
11617
11603
|
import { APP_BASE_PATH } from '@/lib/tenantEnv';
|
|
11618
11604
|
import type { PageConfig, SiteConfig } from '@/types';
|
|
@@ -11625,7 +11611,8 @@ type UseAdminStudioContentOptions = {
|
|
|
11625
11611
|
apiKey: string;
|
|
11626
11612
|
setPages: Dispatch<SetStateAction<Record<string, PageConfig>>>;
|
|
11627
11613
|
setSiteConfig: Dispatch<SetStateAction<SiteConfig>>;
|
|
11628
|
-
|
|
11614
|
+
/** Fired when /admin may paint — after cache hit and/or network attempt settles. */
|
|
11615
|
+
onSettled?: () => void;
|
|
11629
11616
|
};
|
|
11630
11617
|
|
|
11631
11618
|
/** Studio `/admin` sync via legacy `/content` — never mixed into visitor `/render` bootstrap. */
|
|
@@ -11635,13 +11622,28 @@ export function useAdminStudioContent({
|
|
|
11635
11622
|
apiKey,
|
|
11636
11623
|
setPages,
|
|
11637
11624
|
setSiteConfig,
|
|
11638
|
-
|
|
11625
|
+
onSettled,
|
|
11639
11626
|
}: UseAdminStudioContentOptions) {
|
|
11640
11627
|
const loadedRef = useRef(false);
|
|
11641
11628
|
const inFlightRef = useRef<Promise<void> | null>(null);
|
|
11629
|
+
const settledRef = useRef(false);
|
|
11642
11630
|
|
|
11643
11631
|
useEffect(() => {
|
|
11644
|
-
|
|
11632
|
+
settledRef.current = false;
|
|
11633
|
+
loadedRef.current = false;
|
|
11634
|
+
|
|
11635
|
+
if (!enabled) return;
|
|
11636
|
+
|
|
11637
|
+
const settle = () => {
|
|
11638
|
+
if (settledRef.current) return;
|
|
11639
|
+
settledRef.current = true;
|
|
11640
|
+
onSettled?.();
|
|
11641
|
+
};
|
|
11642
|
+
|
|
11643
|
+
if (apiCandidates.length === 0 || !apiKey.trim()) {
|
|
11644
|
+
settle();
|
|
11645
|
+
return;
|
|
11646
|
+
}
|
|
11645
11647
|
|
|
11646
11648
|
const syncIfAdmin = () => {
|
|
11647
11649
|
if (!isAdminPath(window.location.pathname, APP_BASE_PATH)) return;
|
|
@@ -11650,6 +11652,14 @@ export function useAdminStudioContent({
|
|
|
11650
11652
|
const controller = new AbortController();
|
|
11651
11653
|
const fingerprint = cloudFingerprint(apiCandidates[0]!, apiKey);
|
|
11652
11654
|
|
|
11655
|
+
// Paint from cache immediately when present — then refresh from network.
|
|
11656
|
+
const { cachedPages, cachedSite } = readCachedPages(fingerprint);
|
|
11657
|
+
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
11658
|
+
setPages(cachedPages);
|
|
11659
|
+
if (cachedSite) setSiteConfig(cachedSite);
|
|
11660
|
+
settle();
|
|
11661
|
+
}
|
|
11662
|
+
|
|
11653
11663
|
inFlightRef.current = fetchLegacyCloudContentPayload(
|
|
11654
11664
|
apiCandidates,
|
|
11655
11665
|
apiKey,
|
|
@@ -11676,6 +11686,7 @@ export function useAdminStudioContent({
|
|
|
11676
11686
|
})
|
|
11677
11687
|
.finally(() => {
|
|
11678
11688
|
inFlightRef.current = null;
|
|
11689
|
+
settle();
|
|
11679
11690
|
});
|
|
11680
11691
|
};
|
|
11681
11692
|
|
|
@@ -11685,7 +11696,7 @@ export function useAdminStudioContent({
|
|
|
11685
11696
|
unpatch();
|
|
11686
11697
|
inFlightRef.current = null;
|
|
11687
11698
|
};
|
|
11688
|
-
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig,
|
|
11699
|
+
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig, onSettled]);
|
|
11689
11700
|
}
|
|
11690
11701
|
|
|
11691
11702
|
END_OF_FILE_CONTENT
|
|
@@ -11893,40 +11904,31 @@ END_OF_FILE_CONTENT
|
|
|
11893
11904
|
mkdir -p "src/lib/spp"
|
|
11894
11905
|
echo "Creating src/lib/spp/cloudConfig.ts..."
|
|
11895
11906
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/spp/cloudConfig.ts"
|
|
11896
|
-
|
|
11897
|
-
return raw.trim().replace(/\/+$/, '');
|
|
11898
|
-
}
|
|
11907
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
11899
11908
|
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
return Array.from(new Set([withApi, base].filter(Boolean)));
|
|
11904
|
-
}
|
|
11909
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
11910
|
+
|
|
11911
|
+
export { buildApiCandidates };
|
|
11905
11912
|
|
|
11913
|
+
/**
|
|
11914
|
+
* Browser-runtime SPP cloud slices.
|
|
11915
|
+
* Credentials ⇒ cloud usable. Save2Repo does not mean “cloud off”
|
|
11916
|
+
* (boot may still be static; live slices remain available).
|
|
11917
|
+
* SSG/bake: local JSON only (SPP §3).
|
|
11918
|
+
*/
|
|
11906
11919
|
export function getSppCloudConfig(): {
|
|
11907
11920
|
enabled: boolean;
|
|
11908
11921
|
apiBases: string[];
|
|
11909
11922
|
apiKey: string;
|
|
11910
11923
|
} {
|
|
11911
|
-
|
|
11912
|
-
import.meta.env.VITE_OLONJS_CLOUD_URL?.trim() ||
|
|
11913
|
-
import.meta.env.VITE_JSONPAGES_CLOUD_URL?.trim() ||
|
|
11914
|
-
'';
|
|
11915
|
-
const apiKey =
|
|
11916
|
-
import.meta.env.VITE_OLONJS_API_KEY?.trim() ||
|
|
11917
|
-
import.meta.env.VITE_JSONPAGES_API_KEY?.trim() ||
|
|
11918
|
-
'';
|
|
11919
|
-
const save2Repo = import.meta.env.VITE_SAVE2REPO === 'true';
|
|
11920
|
-
|
|
11921
|
-
// SSG/bake: local resolved JSON only. Cloud slices are browser-runtime (SPP §3).
|
|
11922
|
-
if (import.meta.env.SSR || !apiUrl || !apiKey || save2Repo) {
|
|
11924
|
+
if (import.meta.env.SSR || !cloudPolicy.isCloudMode) {
|
|
11923
11925
|
return { enabled: false, apiBases: [], apiKey: '' };
|
|
11924
11926
|
}
|
|
11925
11927
|
|
|
11926
11928
|
return {
|
|
11927
11929
|
enabled: true,
|
|
11928
|
-
apiBases: buildApiCandidates(apiUrl),
|
|
11929
|
-
apiKey,
|
|
11930
|
+
apiBases: buildApiCandidates(cloudPolicy.apiUrl),
|
|
11931
|
+
apiKey: cloudPolicy.apiKey,
|
|
11930
11932
|
};
|
|
11931
11933
|
}
|
|
11932
11934
|
|
|
@@ -13092,12 +13094,16 @@ END_OF_FILE_CONTENT
|
|
|
13092
13094
|
echo "Creating src/lib/tenantEnv.ts..."
|
|
13093
13095
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/tenantEnv.ts"
|
|
13094
13096
|
import { normalizeBasePath } from '@olonjs/core';
|
|
13097
|
+
import { readCloudEnvFromVite, resolveCloudPolicy, type CloudPolicy } from '@olonjs/react';
|
|
13095
13098
|
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13099
|
+
/** Single Vite → policy path. Prefer `cloudPolicy` over ad-hoc env reads. */
|
|
13100
|
+
export const cloudPolicy: CloudPolicy = resolveCloudPolicy(
|
|
13101
|
+
readCloudEnvFromVite(import.meta.env as Record<string, unknown>),
|
|
13102
|
+
);
|
|
13103
|
+
|
|
13104
|
+
/** Aliases of `cloudPolicy.apiUrl` / `apiKey` for DNA helpers. */
|
|
13105
|
+
export const CLOUD_API_URL = cloudPolicy.apiUrl;
|
|
13106
|
+
export const CLOUD_API_KEY = cloudPolicy.apiKey;
|
|
13101
13107
|
export const APP_BASE_PATH = normalizeBasePath(import.meta.env.BASE_URL || '/');
|
|
13102
13108
|
export const TENANT_ID = 'alpha';
|
|
13103
13109
|
|
|
@@ -13106,28 +13112,33 @@ echo "Creating src/lib/useAssetsManifest.ts..."
|
|
|
13106
13112
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useAssetsManifest.ts"
|
|
13107
13113
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
13108
13114
|
import type { LibraryImageEntry } from '@olonjs/core';
|
|
13109
|
-
import { buildApiCandidates } from '
|
|
13110
|
-
|
|
13115
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
13116
|
+
|
|
13117
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13111
13118
|
|
|
13112
13119
|
function normalizeApiBase(raw: string): string {
|
|
13113
13120
|
return raw.trim().replace(/\/+$/, '');
|
|
13114
13121
|
}
|
|
13115
13122
|
|
|
13116
|
-
|
|
13123
|
+
/** Asset library — cloud list when `cloudPolicy.isCloudMode`, else local `/api/list-assets`. */
|
|
13124
|
+
export function useAssetsManifest(isCloudMode: boolean = cloudPolicy.isCloudMode) {
|
|
13117
13125
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
13118
13126
|
const cloudApiCandidates = useMemo(
|
|
13119
|
-
() => (isCloudMode &&
|
|
13127
|
+
() => (isCloudMode && cloudPolicy.apiUrl ? buildApiCandidates(cloudPolicy.apiUrl) : []),
|
|
13120
13128
|
[isCloudMode],
|
|
13121
13129
|
);
|
|
13122
13130
|
|
|
13123
13131
|
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
13124
|
-
if (isCloudMode &&
|
|
13125
|
-
const apiBases =
|
|
13132
|
+
if (isCloudMode && cloudPolicy.apiUrl && cloudPolicy.apiKey) {
|
|
13133
|
+
const apiBases =
|
|
13134
|
+
cloudApiCandidates.length > 0
|
|
13135
|
+
? cloudApiCandidates
|
|
13136
|
+
: [normalizeApiBase(cloudPolicy.apiUrl)];
|
|
13126
13137
|
for (const apiBase of apiBases) {
|
|
13127
13138
|
try {
|
|
13128
13139
|
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
13129
13140
|
method: 'GET',
|
|
13130
|
-
headers: { Authorization: `Bearer ${
|
|
13141
|
+
headers: { Authorization: `Bearer ${cloudPolicy.apiKey}` },
|
|
13131
13142
|
});
|
|
13132
13143
|
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
13133
13144
|
if (!res.ok) continue;
|
|
@@ -13401,6 +13412,8 @@ echo "Creating src/lib/useFormSubmit.ts..."
|
|
|
13401
13412
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useFormSubmit.ts"
|
|
13402
13413
|
import { useState, useCallback } from 'react';
|
|
13403
13414
|
|
|
13415
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13416
|
+
|
|
13404
13417
|
export type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
|
|
13405
13418
|
|
|
13406
13419
|
interface UseFormSubmitOptions {
|
|
@@ -13413,22 +13426,20 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13413
13426
|
const [message, setMessage] = useState<string>('');
|
|
13414
13427
|
|
|
13415
13428
|
const submit = useCallback(async (
|
|
13416
|
-
formData: FormData,
|
|
13417
|
-
recipientEmail: string,
|
|
13418
|
-
pageSlug: string,
|
|
13429
|
+
formData: FormData,
|
|
13430
|
+
recipientEmail: string,
|
|
13431
|
+
pageSlug: string,
|
|
13419
13432
|
sectionId: string
|
|
13420
13433
|
) => {
|
|
13421
|
-
const
|
|
13422
|
-
const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
|
|
13434
|
+
const { apiUrl, apiKey } = cloudPolicy;
|
|
13423
13435
|
|
|
13424
|
-
if (!
|
|
13436
|
+
if (!apiUrl || !apiKey) {
|
|
13425
13437
|
setStatus('error');
|
|
13426
13438
|
setMessage('Configurazione API non disponibile. Riprova tra poco.');
|
|
13427
13439
|
return false;
|
|
13428
13440
|
}
|
|
13429
13441
|
|
|
13430
|
-
|
|
13431
|
-
const data: Record<string, any> = {};
|
|
13442
|
+
const data: Record<string, string> = {};
|
|
13432
13443
|
formData.forEach((value, key) => {
|
|
13433
13444
|
data[key] = String(value).trim();
|
|
13434
13445
|
});
|
|
@@ -13443,18 +13454,17 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13443
13454
|
submittedAt: new Date().toISOString(),
|
|
13444
13455
|
};
|
|
13445
13456
|
|
|
13446
|
-
// Idempotency Key per evitare doppi invii accidentali
|
|
13447
13457
|
const idempotencyKey = `form-${sectionId}-${Date.now()}`;
|
|
13448
13458
|
|
|
13449
13459
|
setStatus('submitting');
|
|
13450
13460
|
setMessage('Invio in corso...');
|
|
13451
13461
|
|
|
13452
13462
|
try {
|
|
13453
|
-
const apiBase =
|
|
13463
|
+
const apiBase = apiUrl.replace(/\/$/, '');
|
|
13454
13464
|
const response = await fetch(`${apiBase}/forms/submit`, {
|
|
13455
13465
|
method: 'POST',
|
|
13456
13466
|
headers: {
|
|
13457
|
-
Authorization: `Bearer ${
|
|
13467
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13458
13468
|
'Content-Type': 'application/json',
|
|
13459
13469
|
'Idempotency-Key': idempotencyKey,
|
|
13460
13470
|
},
|
|
@@ -13484,23 +13494,18 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13484
13494
|
}, []);
|
|
13485
13495
|
|
|
13486
13496
|
return { submit, status, message, reset };
|
|
13487
|
-
}
|
|
13497
|
+
}
|
|
13498
|
+
|
|
13488
13499
|
END_OF_FILE_CONTENT
|
|
13489
13500
|
echo "Creating src/lib/useOlonForms.ts..."
|
|
13490
13501
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
13491
13502
|
import { useCallback, useEffect, useState } from 'react';
|
|
13492
13503
|
import type { FormState } from '@olonjs/react';
|
|
13493
13504
|
|
|
13494
|
-
|
|
13495
|
-
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
13496
|
-
(import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
|
|
13497
|
-
|
|
13498
|
-
const API_KEY =
|
|
13499
|
-
(import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
|
|
13500
|
-
(import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
|
|
13505
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13501
13506
|
|
|
13502
13507
|
interface UseOlonFormsOptions {
|
|
13503
|
-
/** Override the submit endpoint. Defaults to
|
|
13508
|
+
/** Override the submit endpoint. Defaults to cloudPolicy.apiUrl/forms/submit */
|
|
13504
13509
|
endpoint?: string;
|
|
13505
13510
|
}
|
|
13506
13511
|
|
|
@@ -13519,13 +13524,16 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13519
13524
|
}, []);
|
|
13520
13525
|
|
|
13521
13526
|
useEffect(() => {
|
|
13527
|
+
const apiUrl = cloudPolicy.apiUrl;
|
|
13528
|
+
const apiKey = cloudPolicy.apiKey;
|
|
13529
|
+
|
|
13522
13530
|
const resolvedBase = options?.endpoint
|
|
13523
13531
|
? options.endpoint.replace(/\/$/, '')
|
|
13524
|
-
:
|
|
13525
|
-
?
|
|
13532
|
+
: apiUrl
|
|
13533
|
+
? apiUrl.replace(/\/$/, '')
|
|
13526
13534
|
: null;
|
|
13527
13535
|
|
|
13528
|
-
if (!resolvedBase || !
|
|
13536
|
+
if (!resolvedBase || !apiKey) {
|
|
13529
13537
|
console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
|
|
13530
13538
|
return;
|
|
13531
13539
|
}
|
|
@@ -13568,7 +13576,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13568
13576
|
const response = await fetch(endpoint, {
|
|
13569
13577
|
method: 'POST',
|
|
13570
13578
|
headers: {
|
|
13571
|
-
Authorization: `Bearer ${
|
|
13579
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13572
13580
|
'Content-Type': 'application/json',
|
|
13573
13581
|
'Idempotency-Key': `form-${formId}-${Date.now()}`,
|
|
13574
13582
|
},
|
|
@@ -13619,7 +13627,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13619
13627
|
END_OF_FILE_CONTENT
|
|
13620
13628
|
echo "Creating src/lib/useTenantBootstrap.ts..."
|
|
13621
13629
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useTenantBootstrap.ts"
|
|
13622
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13630
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13623
13631
|
|
|
13624
13632
|
import { logBootstrapEvent, toCloudLoadFailure } from '@/lib/cloud/bootstrapTelemetry';
|
|
13625
13633
|
|
|
@@ -13649,7 +13657,7 @@ import {
|
|
|
13649
13657
|
|
|
13650
13658
|
import type { JsonPagesConfig } from '@olonjs/core';
|
|
13651
13659
|
|
|
13652
|
-
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL,
|
|
13660
|
+
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL, cloudPolicy } from '@/lib/tenantEnv';
|
|
13653
13661
|
|
|
13654
13662
|
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
13655
13663
|
|
|
@@ -13731,13 +13739,10 @@ export function useTenantBootstrap({
|
|
|
13731
13739
|
|
|
13732
13740
|
}: UseTenantBootstrapOptions) {
|
|
13733
13741
|
|
|
13734
|
-
const isCloudMode
|
|
13735
|
-
|
|
13736
|
-
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
13742
|
+
const { isCloudMode, bootSource } = cloudPolicy;
|
|
13737
13743
|
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
const useRenderBootstrap = isHotSaveMode;
|
|
13744
|
+
/** Live SPP render boot — only when bootSource is live (not static Save2Repo). */
|
|
13745
|
+
const useRenderBootstrap = bootSource === 'live';
|
|
13741
13746
|
|
|
13742
13747
|
|
|
13743
13748
|
|
|
@@ -13801,7 +13806,11 @@ export function useTenantBootstrap({
|
|
|
13801
13806
|
|
|
13802
13807
|
const isTenantEmpty = Object.keys(pages).length === 0;
|
|
13803
13808
|
|
|
13804
|
-
|
|
13809
|
+
/** Call when live /admin content sync has finished (success, cache, or error). */
|
|
13810
|
+
const markCloudContentReady = useCallback(() => {
|
|
13811
|
+
setShowTopProgress(false);
|
|
13812
|
+
setHasInitialCloudResolved(true);
|
|
13813
|
+
}, []);
|
|
13805
13814
|
|
|
13806
13815
|
const retryBootstrap = () => {
|
|
13807
13816
|
|
|
@@ -13841,7 +13850,7 @@ export function useTenantBootstrap({
|
|
|
13841
13850
|
|
|
13842
13851
|
|
|
13843
13852
|
|
|
13844
|
-
if (
|
|
13853
|
+
if (bootSource === 'static') {
|
|
13845
13854
|
|
|
13846
13855
|
if (contentLoadInFlight.current) return;
|
|
13847
13856
|
|
|
@@ -13938,17 +13947,12 @@ export function useTenantBootstrap({
|
|
|
13938
13947
|
|
|
13939
13948
|
|
|
13940
13949
|
if (isAdminPath(window.location.pathname, APP_BASE_PATH)) {
|
|
13941
|
-
|
|
13950
|
+
// Live /admin: content comes from useAdminStudioContent — do not paint empty first.
|
|
13942
13951
|
setContentMode('cloud');
|
|
13943
|
-
|
|
13944
13952
|
setContentFallback(null);
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
setHasInitialCloudResolved(true);
|
|
13949
|
-
|
|
13953
|
+
setShowTopProgress(true);
|
|
13954
|
+
setHasInitialCloudResolved(false);
|
|
13950
13955
|
return;
|
|
13951
|
-
|
|
13952
13956
|
}
|
|
13953
13957
|
|
|
13954
13958
|
|
|
@@ -14245,7 +14249,7 @@ export function useTenantBootstrap({
|
|
|
14245
14249
|
|
|
14246
14250
|
isCloudMode,
|
|
14247
14251
|
|
|
14248
|
-
|
|
14252
|
+
bootSource,
|
|
14249
14253
|
|
|
14250
14254
|
useRenderBootstrap,
|
|
14251
14255
|
|
|
@@ -14291,9 +14295,7 @@ export function useTenantBootstrap({
|
|
|
14291
14295
|
|
|
14292
14296
|
isCloudMode,
|
|
14293
14297
|
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
isHotSaveMode,
|
|
14298
|
+
bootSource,
|
|
14297
14299
|
|
|
14298
14300
|
contentMode,
|
|
14299
14301
|
|
|
@@ -14307,6 +14309,8 @@ export function useTenantBootstrap({
|
|
|
14307
14309
|
|
|
14308
14310
|
isTenantEmpty,
|
|
14309
14311
|
|
|
14312
|
+
markCloudContentReady,
|
|
14313
|
+
|
|
14310
14314
|
retryBootstrap,
|
|
14311
14315
|
|
|
14312
14316
|
};
|
|
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1821
|
-
"@olonjs/react": "^0.1.
|
|
1822
|
-
"@olonjs/studio": "^0.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.19",
|
|
1821
|
+
"@olonjs/react": "^0.1.2",
|
|
1822
|
+
"@olonjs/studio": "^0.1.2",
|
|
1823
1823
|
"class-variance-authority": "^0.7.1",
|
|
1824
1824
|
"clsx": "^2.1.1",
|
|
1825
1825
|
"lucide-react": "^0.474.0",
|
|
@@ -2952,7 +2952,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
|
|
|
2952
2952
|
* Bootstrap, persistence, and engine wiring — logic lives in lib/hooks.
|
|
2953
2953
|
*/
|
|
2954
2954
|
import { useEffect, useMemo } from 'react';
|
|
2955
|
-
import { JsonPagesEngine, OlonFormsContext } from '@olonjs/react';
|
|
2955
|
+
import { JsonPagesEngine, OlonFormsContext, createHotSaveHandler } from '@olonjs/react';
|
|
2956
2956
|
import type { JsonPagesConfig, ProjectState } from '@olonjs/core';
|
|
2957
2957
|
import { withBasePath } from '@olonjs/core';
|
|
2958
2958
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
@@ -2985,7 +2985,7 @@ import {
|
|
|
2985
2985
|
useInjectedTenantCss,
|
|
2986
2986
|
useTenantFontsReady,
|
|
2987
2987
|
} from '@/lib/tenantCss';
|
|
2988
|
-
import { APP_BASE_PATH,
|
|
2988
|
+
import { APP_BASE_PATH, TENANT_ID, cloudPolicy } from '@/lib/tenantEnv';
|
|
2989
2989
|
import { useAssetsManifest } from '@/lib/useAssetsManifest';
|
|
2990
2990
|
import { useCloudSave } from '@/lib/useCloudSave';
|
|
2991
2991
|
import { useTenantBootstrap } from '@/lib/useTenantBootstrap';
|
|
@@ -3010,16 +3010,40 @@ function App() {
|
|
|
3010
3010
|
themeConfigSeed,
|
|
3011
3011
|
});
|
|
3012
3012
|
useAdminStudioContent({
|
|
3013
|
-
enabled: bootstrap.
|
|
3013
|
+
enabled: bootstrap.bootSource === 'live',
|
|
3014
3014
|
apiCandidates: bootstrap.cloudApiCandidates,
|
|
3015
|
-
apiKey:
|
|
3015
|
+
apiKey: cloudPolicy.apiKey,
|
|
3016
3016
|
setPages: bootstrap.setPages,
|
|
3017
3017
|
setSiteConfig: bootstrap.setSiteConfig,
|
|
3018
|
-
|
|
3018
|
+
onSettled: bootstrap.markCloudContentReady,
|
|
3019
3019
|
});
|
|
3020
|
-
const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(
|
|
3020
|
+
const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(cloudPolicy.isCloudMode);
|
|
3021
3021
|
const { cloudSaveUi, runCloudSave, closeCloudDrawer, retryCloudSave } = useCloudSave();
|
|
3022
3022
|
|
|
3023
|
+
const hotSave = useMemo(
|
|
3024
|
+
() =>
|
|
3025
|
+
createHotSaveHandler({
|
|
3026
|
+
apiUrl: cloudPolicy.apiUrl,
|
|
3027
|
+
apiKey: cloudPolicy.apiKey,
|
|
3028
|
+
onSuccess: ({ state, slug, apiUrl, apiKey }) => {
|
|
3029
|
+
const keyFingerprint = cloudFingerprintFromUrl(apiUrl, apiKey);
|
|
3030
|
+
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3031
|
+
const existing = readCachedCloudContent(keyFingerprint);
|
|
3032
|
+
writeCachedCloudContent({
|
|
3033
|
+
keyFingerprint,
|
|
3034
|
+
savedAt: Date.now(),
|
|
3035
|
+
siteConfig: state.site ?? null,
|
|
3036
|
+
collections: state.collections,
|
|
3037
|
+
pages: {
|
|
3038
|
+
...(existing?.pages ?? {}),
|
|
3039
|
+
[normalizedSlug]: state.page,
|
|
3040
|
+
},
|
|
3041
|
+
});
|
|
3042
|
+
},
|
|
3043
|
+
}),
|
|
3044
|
+
[],
|
|
3045
|
+
);
|
|
3046
|
+
|
|
3023
3047
|
const tenantCssParts = useMemo(() => extractLeadingRemoteCssImports(tenantCss), []);
|
|
3024
3048
|
const resolvedTenantCss = useMemo(
|
|
3025
3049
|
() => [buildThemeFontVarsCss(bootstrap.themeConfig), tenantCssParts.rest].filter(Boolean).join('\n'),
|
|
@@ -3058,11 +3082,10 @@ function App() {
|
|
|
3058
3082
|
}, [canPaintVisitor, bootstrap.enginePages, bootstrap.siteConfig]);
|
|
3059
3083
|
|
|
3060
3084
|
const engineCollections =
|
|
3061
|
-
bootstrap.
|
|
3085
|
+
bootstrap.bootSource === 'live' || Object.keys(bootstrap.collections).length > 0
|
|
3062
3086
|
? bootstrap.collections
|
|
3063
3087
|
: fileCollections;
|
|
3064
3088
|
|
|
3065
|
-
// TOASK: why this if we have a menuConfig Object?
|
|
3066
3089
|
const engineRefDocuments = useMemo(
|
|
3067
3090
|
() => ({
|
|
3068
3091
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3071,7 +3094,6 @@ function App() {
|
|
|
3071
3094
|
}),
|
|
3072
3095
|
[bootstrap.menuConfig],
|
|
3073
3096
|
);
|
|
3074
|
-
// TOASK END
|
|
3075
3097
|
const config: JsonPagesConfig = {
|
|
3076
3098
|
tenantId: TENANT_ID,
|
|
3077
3099
|
basePath: APP_BASE_PATH,
|
|
@@ -3110,48 +3132,13 @@ function App() {
|
|
|
3110
3132
|
setCollections: bootstrap.setCollections,
|
|
3111
3133
|
});
|
|
3112
3134
|
},
|
|
3113
|
-
|
|
3114
|
-
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3115
|
-
throw new Error('Cloud mode is not configured for hot save.');
|
|
3116
|
-
}
|
|
3117
|
-
const apiBase = CLOUD_API_URL.replace(/\/$/, '');
|
|
3118
|
-
const res = await fetch(`${apiBase}/hotSave`, {
|
|
3119
|
-
method: 'POST',
|
|
3120
|
-
headers: {
|
|
3121
|
-
'Content-Type': 'application/json',
|
|
3122
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3123
|
-
},
|
|
3124
|
-
body: JSON.stringify({
|
|
3125
|
-
slug,
|
|
3126
|
-
page: state.page,
|
|
3127
|
-
siteConfig: state.site,
|
|
3128
|
-
collections: state.collections,
|
|
3129
|
-
}),
|
|
3130
|
-
});
|
|
3131
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
3132
|
-
if (!res.ok) {
|
|
3133
|
-
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
3134
|
-
}
|
|
3135
|
-
const keyFingerprint = cloudFingerprintFromUrl(CLOUD_API_URL, CLOUD_API_KEY);
|
|
3136
|
-
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3137
|
-
const existing = readCachedCloudContent(keyFingerprint);
|
|
3138
|
-
writeCachedCloudContent({
|
|
3139
|
-
keyFingerprint,
|
|
3140
|
-
savedAt: Date.now(),
|
|
3141
|
-
siteConfig: state.site ?? null,
|
|
3142
|
-
collections: state.collections,
|
|
3143
|
-
pages: {
|
|
3144
|
-
...(existing?.pages ?? {}),
|
|
3145
|
-
[normalizedSlug]: state.page,
|
|
3146
|
-
},
|
|
3147
|
-
});
|
|
3148
|
-
},
|
|
3135
|
+
hotSave,
|
|
3149
3136
|
async coldSave(state: ProjectState, slug: string): Promise<void> {
|
|
3150
3137
|
await runCloudSave({ state, slug }, true);
|
|
3151
3138
|
},
|
|
3152
|
-
showLocalSave:
|
|
3153
|
-
showHotSave:
|
|
3154
|
-
showColdSave:
|
|
3139
|
+
showLocalSave: cloudPolicy.showLocalSave,
|
|
3140
|
+
showHotSave: cloudPolicy.showHotSave,
|
|
3141
|
+
showColdSave: cloudPolicy.showColdSave,
|
|
3155
3142
|
},
|
|
3156
3143
|
assets: {
|
|
3157
3144
|
assetsBaseUrl: withBasePath('/assets', APP_BASE_PATH),
|
|
@@ -3159,9 +3146,9 @@ function App() {
|
|
|
3159
3146
|
onAssetUpload: (file) =>
|
|
3160
3147
|
uploadTenantAsset(file, {
|
|
3161
3148
|
basePath: APP_BASE_PATH,
|
|
3162
|
-
isCloudMode:
|
|
3163
|
-
cloudApiUrl:
|
|
3164
|
-
cloudApiKey:
|
|
3149
|
+
isCloudMode: cloudPolicy.isCloudMode,
|
|
3150
|
+
cloudApiUrl: cloudPolicy.apiUrl,
|
|
3151
|
+
cloudApiKey: cloudPolicy.apiKey,
|
|
3165
3152
|
apiBases: cloudApiCandidates,
|
|
3166
3153
|
onUploaded: loadAssetsManifest,
|
|
3167
3154
|
}),
|
|
@@ -3172,7 +3159,7 @@ function App() {
|
|
|
3172
3159
|
<ThemeProvider>
|
|
3173
3160
|
<OlonFormsContext.Provider value={formStates}>
|
|
3174
3161
|
<TenantBootstrapChrome
|
|
3175
|
-
isCloudMode={
|
|
3162
|
+
isCloudMode={cloudPolicy.isCloudMode}
|
|
3176
3163
|
showTopProgress={bootstrap.showTopProgress || (bootstrap.shouldRenderEngine && !fontsReady)}
|
|
3177
3164
|
contentMode={bootstrap.contentMode}
|
|
3178
3165
|
contentFallback={bootstrap.contentFallback}
|
|
@@ -4089,15 +4076,15 @@ echo "Creating src/components/form-demo/View.tsx..."
|
|
|
4089
4076
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
4090
4077
|
import { Icon } from '@/lib/IconResolver';
|
|
4091
4078
|
import { useFormState } from '@olonjs/react';
|
|
4079
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
4092
4080
|
import type { FormDemoData } from './types';
|
|
4093
4081
|
|
|
4094
4082
|
type FormDemoViewProps = {
|
|
4095
4083
|
data: FormDemoData;
|
|
4096
4084
|
};
|
|
4097
4085
|
|
|
4098
|
-
const
|
|
4099
|
-
|
|
4100
|
-
!import.meta.env.VITE_OLONJS_CLOUD_URL;
|
|
4086
|
+
const missingCloudUrl = !cloudPolicy.apiUrl;
|
|
4087
|
+
const hasCloudKey = Boolean(cloudPolicy.apiKey);
|
|
4101
4088
|
|
|
4102
4089
|
function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
4103
4090
|
const steps = [
|
|
@@ -4107,14 +4094,14 @@ function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
|
4107
4094
|
code: '"recipientEmail": "tu@esempio.it"',
|
|
4108
4095
|
},
|
|
4109
4096
|
{
|
|
4110
|
-
done: !
|
|
4111
|
-
label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
|
|
4112
|
-
code: '
|
|
4097
|
+
done: !missingCloudUrl,
|
|
4098
|
+
label: 'VITE_OLONJS_CLOUD_URL (o VITE_JSONPAGES_CLOUD_URL) nel file .env',
|
|
4099
|
+
code: 'VITE_OLONJS_CLOUD_URL=https://cloud.olonjs.io/api/v1',
|
|
4113
4100
|
},
|
|
4114
4101
|
{
|
|
4115
|
-
done:
|
|
4116
|
-
label: 'VITE_JSONPAGES_API_KEY nel file .env',
|
|
4117
|
-
code: '
|
|
4102
|
+
done: hasCloudKey,
|
|
4103
|
+
label: 'VITE_OLONJS_API_KEY (o VITE_JSONPAGES_API_KEY) nel file .env',
|
|
4104
|
+
code: 'VITE_OLONJS_API_KEY=sk-...',
|
|
4118
4105
|
},
|
|
4119
4106
|
];
|
|
4120
4107
|
|
|
@@ -11610,9 +11597,8 @@ echo "Creating src/lib/cloud/useAdminStudioContent.ts..."
|
|
|
11610
11597
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloud/useAdminStudioContent.ts"
|
|
11611
11598
|
import { useEffect, useRef } from 'react';
|
|
11612
11599
|
import type { Dispatch, SetStateAction } from 'react';
|
|
11613
|
-
import type { JsonPagesConfig } from '@olonjs/core';
|
|
11614
11600
|
import { applyLegacyCloudPayload, fetchLegacyCloudContentPayload } from '@/lib/cloud/cloudContentClient';
|
|
11615
|
-
import { cloudFingerprint, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11601
|
+
import { cloudFingerprint, readCachedPages, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11616
11602
|
import { isAdminPath, patchHistoryNavigation } from '@/lib/spp';
|
|
11617
11603
|
import { APP_BASE_PATH } from '@/lib/tenantEnv';
|
|
11618
11604
|
import type { PageConfig, SiteConfig } from '@/types';
|
|
@@ -11625,7 +11611,8 @@ type UseAdminStudioContentOptions = {
|
|
|
11625
11611
|
apiKey: string;
|
|
11626
11612
|
setPages: Dispatch<SetStateAction<Record<string, PageConfig>>>;
|
|
11627
11613
|
setSiteConfig: Dispatch<SetStateAction<SiteConfig>>;
|
|
11628
|
-
|
|
11614
|
+
/** Fired when /admin may paint — after cache hit and/or network attempt settles. */
|
|
11615
|
+
onSettled?: () => void;
|
|
11629
11616
|
};
|
|
11630
11617
|
|
|
11631
11618
|
/** Studio `/admin` sync via legacy `/content` — never mixed into visitor `/render` bootstrap. */
|
|
@@ -11635,13 +11622,28 @@ export function useAdminStudioContent({
|
|
|
11635
11622
|
apiKey,
|
|
11636
11623
|
setPages,
|
|
11637
11624
|
setSiteConfig,
|
|
11638
|
-
|
|
11625
|
+
onSettled,
|
|
11639
11626
|
}: UseAdminStudioContentOptions) {
|
|
11640
11627
|
const loadedRef = useRef(false);
|
|
11641
11628
|
const inFlightRef = useRef<Promise<void> | null>(null);
|
|
11629
|
+
const settledRef = useRef(false);
|
|
11642
11630
|
|
|
11643
11631
|
useEffect(() => {
|
|
11644
|
-
|
|
11632
|
+
settledRef.current = false;
|
|
11633
|
+
loadedRef.current = false;
|
|
11634
|
+
|
|
11635
|
+
if (!enabled) return;
|
|
11636
|
+
|
|
11637
|
+
const settle = () => {
|
|
11638
|
+
if (settledRef.current) return;
|
|
11639
|
+
settledRef.current = true;
|
|
11640
|
+
onSettled?.();
|
|
11641
|
+
};
|
|
11642
|
+
|
|
11643
|
+
if (apiCandidates.length === 0 || !apiKey.trim()) {
|
|
11644
|
+
settle();
|
|
11645
|
+
return;
|
|
11646
|
+
}
|
|
11645
11647
|
|
|
11646
11648
|
const syncIfAdmin = () => {
|
|
11647
11649
|
if (!isAdminPath(window.location.pathname, APP_BASE_PATH)) return;
|
|
@@ -11650,6 +11652,14 @@ export function useAdminStudioContent({
|
|
|
11650
11652
|
const controller = new AbortController();
|
|
11651
11653
|
const fingerprint = cloudFingerprint(apiCandidates[0]!, apiKey);
|
|
11652
11654
|
|
|
11655
|
+
// Paint from cache immediately when present — then refresh from network.
|
|
11656
|
+
const { cachedPages, cachedSite } = readCachedPages(fingerprint);
|
|
11657
|
+
if (cachedPages && Object.keys(cachedPages).length > 0) {
|
|
11658
|
+
setPages(cachedPages);
|
|
11659
|
+
if (cachedSite) setSiteConfig(cachedSite);
|
|
11660
|
+
settle();
|
|
11661
|
+
}
|
|
11662
|
+
|
|
11653
11663
|
inFlightRef.current = fetchLegacyCloudContentPayload(
|
|
11654
11664
|
apiCandidates,
|
|
11655
11665
|
apiKey,
|
|
@@ -11676,6 +11686,7 @@ export function useAdminStudioContent({
|
|
|
11676
11686
|
})
|
|
11677
11687
|
.finally(() => {
|
|
11678
11688
|
inFlightRef.current = null;
|
|
11689
|
+
settle();
|
|
11679
11690
|
});
|
|
11680
11691
|
};
|
|
11681
11692
|
|
|
@@ -11685,7 +11696,7 @@ export function useAdminStudioContent({
|
|
|
11685
11696
|
unpatch();
|
|
11686
11697
|
inFlightRef.current = null;
|
|
11687
11698
|
};
|
|
11688
|
-
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig,
|
|
11699
|
+
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig, onSettled]);
|
|
11689
11700
|
}
|
|
11690
11701
|
|
|
11691
11702
|
END_OF_FILE_CONTENT
|
|
@@ -11893,40 +11904,31 @@ END_OF_FILE_CONTENT
|
|
|
11893
11904
|
mkdir -p "src/lib/spp"
|
|
11894
11905
|
echo "Creating src/lib/spp/cloudConfig.ts..."
|
|
11895
11906
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/spp/cloudConfig.ts"
|
|
11896
|
-
|
|
11897
|
-
return raw.trim().replace(/\/+$/, '');
|
|
11898
|
-
}
|
|
11907
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
11899
11908
|
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
return Array.from(new Set([withApi, base].filter(Boolean)));
|
|
11904
|
-
}
|
|
11909
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
11910
|
+
|
|
11911
|
+
export { buildApiCandidates };
|
|
11905
11912
|
|
|
11913
|
+
/**
|
|
11914
|
+
* Browser-runtime SPP cloud slices.
|
|
11915
|
+
* Credentials ⇒ cloud usable. Save2Repo does not mean “cloud off”
|
|
11916
|
+
* (boot may still be static; live slices remain available).
|
|
11917
|
+
* SSG/bake: local JSON only (SPP §3).
|
|
11918
|
+
*/
|
|
11906
11919
|
export function getSppCloudConfig(): {
|
|
11907
11920
|
enabled: boolean;
|
|
11908
11921
|
apiBases: string[];
|
|
11909
11922
|
apiKey: string;
|
|
11910
11923
|
} {
|
|
11911
|
-
|
|
11912
|
-
import.meta.env.VITE_OLONJS_CLOUD_URL?.trim() ||
|
|
11913
|
-
import.meta.env.VITE_JSONPAGES_CLOUD_URL?.trim() ||
|
|
11914
|
-
'';
|
|
11915
|
-
const apiKey =
|
|
11916
|
-
import.meta.env.VITE_OLONJS_API_KEY?.trim() ||
|
|
11917
|
-
import.meta.env.VITE_JSONPAGES_API_KEY?.trim() ||
|
|
11918
|
-
'';
|
|
11919
|
-
const save2Repo = import.meta.env.VITE_SAVE2REPO === 'true';
|
|
11920
|
-
|
|
11921
|
-
// SSG/bake: local resolved JSON only. Cloud slices are browser-runtime (SPP §3).
|
|
11922
|
-
if (import.meta.env.SSR || !apiUrl || !apiKey || save2Repo) {
|
|
11924
|
+
if (import.meta.env.SSR || !cloudPolicy.isCloudMode) {
|
|
11923
11925
|
return { enabled: false, apiBases: [], apiKey: '' };
|
|
11924
11926
|
}
|
|
11925
11927
|
|
|
11926
11928
|
return {
|
|
11927
11929
|
enabled: true,
|
|
11928
|
-
apiBases: buildApiCandidates(apiUrl),
|
|
11929
|
-
apiKey,
|
|
11930
|
+
apiBases: buildApiCandidates(cloudPolicy.apiUrl),
|
|
11931
|
+
apiKey: cloudPolicy.apiKey,
|
|
11930
11932
|
};
|
|
11931
11933
|
}
|
|
11932
11934
|
|
|
@@ -13092,12 +13094,16 @@ END_OF_FILE_CONTENT
|
|
|
13092
13094
|
echo "Creating src/lib/tenantEnv.ts..."
|
|
13093
13095
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/tenantEnv.ts"
|
|
13094
13096
|
import { normalizeBasePath } from '@olonjs/core';
|
|
13097
|
+
import { readCloudEnvFromVite, resolveCloudPolicy, type CloudPolicy } from '@olonjs/react';
|
|
13095
13098
|
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13099
|
+
/** Single Vite → policy path. Prefer `cloudPolicy` over ad-hoc env reads. */
|
|
13100
|
+
export const cloudPolicy: CloudPolicy = resolveCloudPolicy(
|
|
13101
|
+
readCloudEnvFromVite(import.meta.env as Record<string, unknown>),
|
|
13102
|
+
);
|
|
13103
|
+
|
|
13104
|
+
/** Aliases of `cloudPolicy.apiUrl` / `apiKey` for DNA helpers. */
|
|
13105
|
+
export const CLOUD_API_URL = cloudPolicy.apiUrl;
|
|
13106
|
+
export const CLOUD_API_KEY = cloudPolicy.apiKey;
|
|
13101
13107
|
export const APP_BASE_PATH = normalizeBasePath(import.meta.env.BASE_URL || '/');
|
|
13102
13108
|
export const TENANT_ID = 'alpha';
|
|
13103
13109
|
|
|
@@ -13106,28 +13112,33 @@ echo "Creating src/lib/useAssetsManifest.ts..."
|
|
|
13106
13112
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useAssetsManifest.ts"
|
|
13107
13113
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
13108
13114
|
import type { LibraryImageEntry } from '@olonjs/core';
|
|
13109
|
-
import { buildApiCandidates } from '
|
|
13110
|
-
|
|
13115
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
13116
|
+
|
|
13117
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13111
13118
|
|
|
13112
13119
|
function normalizeApiBase(raw: string): string {
|
|
13113
13120
|
return raw.trim().replace(/\/+$/, '');
|
|
13114
13121
|
}
|
|
13115
13122
|
|
|
13116
|
-
|
|
13123
|
+
/** Asset library — cloud list when `cloudPolicy.isCloudMode`, else local `/api/list-assets`. */
|
|
13124
|
+
export function useAssetsManifest(isCloudMode: boolean = cloudPolicy.isCloudMode) {
|
|
13117
13125
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
13118
13126
|
const cloudApiCandidates = useMemo(
|
|
13119
|
-
() => (isCloudMode &&
|
|
13127
|
+
() => (isCloudMode && cloudPolicy.apiUrl ? buildApiCandidates(cloudPolicy.apiUrl) : []),
|
|
13120
13128
|
[isCloudMode],
|
|
13121
13129
|
);
|
|
13122
13130
|
|
|
13123
13131
|
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
13124
|
-
if (isCloudMode &&
|
|
13125
|
-
const apiBases =
|
|
13132
|
+
if (isCloudMode && cloudPolicy.apiUrl && cloudPolicy.apiKey) {
|
|
13133
|
+
const apiBases =
|
|
13134
|
+
cloudApiCandidates.length > 0
|
|
13135
|
+
? cloudApiCandidates
|
|
13136
|
+
: [normalizeApiBase(cloudPolicy.apiUrl)];
|
|
13126
13137
|
for (const apiBase of apiBases) {
|
|
13127
13138
|
try {
|
|
13128
13139
|
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
13129
13140
|
method: 'GET',
|
|
13130
|
-
headers: { Authorization: `Bearer ${
|
|
13141
|
+
headers: { Authorization: `Bearer ${cloudPolicy.apiKey}` },
|
|
13131
13142
|
});
|
|
13132
13143
|
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
13133
13144
|
if (!res.ok) continue;
|
|
@@ -13401,6 +13412,8 @@ echo "Creating src/lib/useFormSubmit.ts..."
|
|
|
13401
13412
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useFormSubmit.ts"
|
|
13402
13413
|
import { useState, useCallback } from 'react';
|
|
13403
13414
|
|
|
13415
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13416
|
+
|
|
13404
13417
|
export type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
|
|
13405
13418
|
|
|
13406
13419
|
interface UseFormSubmitOptions {
|
|
@@ -13413,22 +13426,20 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13413
13426
|
const [message, setMessage] = useState<string>('');
|
|
13414
13427
|
|
|
13415
13428
|
const submit = useCallback(async (
|
|
13416
|
-
formData: FormData,
|
|
13417
|
-
recipientEmail: string,
|
|
13418
|
-
pageSlug: string,
|
|
13429
|
+
formData: FormData,
|
|
13430
|
+
recipientEmail: string,
|
|
13431
|
+
pageSlug: string,
|
|
13419
13432
|
sectionId: string
|
|
13420
13433
|
) => {
|
|
13421
|
-
const
|
|
13422
|
-
const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
|
|
13434
|
+
const { apiUrl, apiKey } = cloudPolicy;
|
|
13423
13435
|
|
|
13424
|
-
if (!
|
|
13436
|
+
if (!apiUrl || !apiKey) {
|
|
13425
13437
|
setStatus('error');
|
|
13426
13438
|
setMessage('Configurazione API non disponibile. Riprova tra poco.');
|
|
13427
13439
|
return false;
|
|
13428
13440
|
}
|
|
13429
13441
|
|
|
13430
|
-
|
|
13431
|
-
const data: Record<string, any> = {};
|
|
13442
|
+
const data: Record<string, string> = {};
|
|
13432
13443
|
formData.forEach((value, key) => {
|
|
13433
13444
|
data[key] = String(value).trim();
|
|
13434
13445
|
});
|
|
@@ -13443,18 +13454,17 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13443
13454
|
submittedAt: new Date().toISOString(),
|
|
13444
13455
|
};
|
|
13445
13456
|
|
|
13446
|
-
// Idempotency Key per evitare doppi invii accidentali
|
|
13447
13457
|
const idempotencyKey = `form-${sectionId}-${Date.now()}`;
|
|
13448
13458
|
|
|
13449
13459
|
setStatus('submitting');
|
|
13450
13460
|
setMessage('Invio in corso...');
|
|
13451
13461
|
|
|
13452
13462
|
try {
|
|
13453
|
-
const apiBase =
|
|
13463
|
+
const apiBase = apiUrl.replace(/\/$/, '');
|
|
13454
13464
|
const response = await fetch(`${apiBase}/forms/submit`, {
|
|
13455
13465
|
method: 'POST',
|
|
13456
13466
|
headers: {
|
|
13457
|
-
Authorization: `Bearer ${
|
|
13467
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13458
13468
|
'Content-Type': 'application/json',
|
|
13459
13469
|
'Idempotency-Key': idempotencyKey,
|
|
13460
13470
|
},
|
|
@@ -13484,23 +13494,18 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13484
13494
|
}, []);
|
|
13485
13495
|
|
|
13486
13496
|
return { submit, status, message, reset };
|
|
13487
|
-
}
|
|
13497
|
+
}
|
|
13498
|
+
|
|
13488
13499
|
END_OF_FILE_CONTENT
|
|
13489
13500
|
echo "Creating src/lib/useOlonForms.ts..."
|
|
13490
13501
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
13491
13502
|
import { useCallback, useEffect, useState } from 'react';
|
|
13492
13503
|
import type { FormState } from '@olonjs/react';
|
|
13493
13504
|
|
|
13494
|
-
|
|
13495
|
-
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
13496
|
-
(import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
|
|
13497
|
-
|
|
13498
|
-
const API_KEY =
|
|
13499
|
-
(import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
|
|
13500
|
-
(import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
|
|
13505
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13501
13506
|
|
|
13502
13507
|
interface UseOlonFormsOptions {
|
|
13503
|
-
/** Override the submit endpoint. Defaults to
|
|
13508
|
+
/** Override the submit endpoint. Defaults to cloudPolicy.apiUrl/forms/submit */
|
|
13504
13509
|
endpoint?: string;
|
|
13505
13510
|
}
|
|
13506
13511
|
|
|
@@ -13519,13 +13524,16 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13519
13524
|
}, []);
|
|
13520
13525
|
|
|
13521
13526
|
useEffect(() => {
|
|
13527
|
+
const apiUrl = cloudPolicy.apiUrl;
|
|
13528
|
+
const apiKey = cloudPolicy.apiKey;
|
|
13529
|
+
|
|
13522
13530
|
const resolvedBase = options?.endpoint
|
|
13523
13531
|
? options.endpoint.replace(/\/$/, '')
|
|
13524
|
-
:
|
|
13525
|
-
?
|
|
13532
|
+
: apiUrl
|
|
13533
|
+
? apiUrl.replace(/\/$/, '')
|
|
13526
13534
|
: null;
|
|
13527
13535
|
|
|
13528
|
-
if (!resolvedBase || !
|
|
13536
|
+
if (!resolvedBase || !apiKey) {
|
|
13529
13537
|
console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
|
|
13530
13538
|
return;
|
|
13531
13539
|
}
|
|
@@ -13568,7 +13576,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13568
13576
|
const response = await fetch(endpoint, {
|
|
13569
13577
|
method: 'POST',
|
|
13570
13578
|
headers: {
|
|
13571
|
-
Authorization: `Bearer ${
|
|
13579
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13572
13580
|
'Content-Type': 'application/json',
|
|
13573
13581
|
'Idempotency-Key': `form-${formId}-${Date.now()}`,
|
|
13574
13582
|
},
|
|
@@ -13619,7 +13627,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13619
13627
|
END_OF_FILE_CONTENT
|
|
13620
13628
|
echo "Creating src/lib/useTenantBootstrap.ts..."
|
|
13621
13629
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useTenantBootstrap.ts"
|
|
13622
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13630
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13623
13631
|
|
|
13624
13632
|
import { logBootstrapEvent, toCloudLoadFailure } from '@/lib/cloud/bootstrapTelemetry';
|
|
13625
13633
|
|
|
@@ -13649,7 +13657,7 @@ import {
|
|
|
13649
13657
|
|
|
13650
13658
|
import type { JsonPagesConfig } from '@olonjs/core';
|
|
13651
13659
|
|
|
13652
|
-
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL,
|
|
13660
|
+
import { APP_BASE_PATH, CLOUD_API_KEY, CLOUD_API_URL, cloudPolicy } from '@/lib/tenantEnv';
|
|
13653
13661
|
|
|
13654
13662
|
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
13655
13663
|
|
|
@@ -13731,13 +13739,10 @@ export function useTenantBootstrap({
|
|
|
13731
13739
|
|
|
13732
13740
|
}: UseTenantBootstrapOptions) {
|
|
13733
13741
|
|
|
13734
|
-
const isCloudMode
|
|
13735
|
-
|
|
13736
|
-
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
13742
|
+
const { isCloudMode, bootSource } = cloudPolicy;
|
|
13737
13743
|
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
const useRenderBootstrap = isHotSaveMode;
|
|
13744
|
+
/** Live SPP render boot — only when bootSource is live (not static Save2Repo). */
|
|
13745
|
+
const useRenderBootstrap = bootSource === 'live';
|
|
13741
13746
|
|
|
13742
13747
|
|
|
13743
13748
|
|
|
@@ -13801,7 +13806,11 @@ export function useTenantBootstrap({
|
|
|
13801
13806
|
|
|
13802
13807
|
const isTenantEmpty = Object.keys(pages).length === 0;
|
|
13803
13808
|
|
|
13804
|
-
|
|
13809
|
+
/** Call when live /admin content sync has finished (success, cache, or error). */
|
|
13810
|
+
const markCloudContentReady = useCallback(() => {
|
|
13811
|
+
setShowTopProgress(false);
|
|
13812
|
+
setHasInitialCloudResolved(true);
|
|
13813
|
+
}, []);
|
|
13805
13814
|
|
|
13806
13815
|
const retryBootstrap = () => {
|
|
13807
13816
|
|
|
@@ -13841,7 +13850,7 @@ export function useTenantBootstrap({
|
|
|
13841
13850
|
|
|
13842
13851
|
|
|
13843
13852
|
|
|
13844
|
-
if (
|
|
13853
|
+
if (bootSource === 'static') {
|
|
13845
13854
|
|
|
13846
13855
|
if (contentLoadInFlight.current) return;
|
|
13847
13856
|
|
|
@@ -13938,17 +13947,12 @@ export function useTenantBootstrap({
|
|
|
13938
13947
|
|
|
13939
13948
|
|
|
13940
13949
|
if (isAdminPath(window.location.pathname, APP_BASE_PATH)) {
|
|
13941
|
-
|
|
13950
|
+
// Live /admin: content comes from useAdminStudioContent — do not paint empty first.
|
|
13942
13951
|
setContentMode('cloud');
|
|
13943
|
-
|
|
13944
13952
|
setContentFallback(null);
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
setHasInitialCloudResolved(true);
|
|
13949
|
-
|
|
13953
|
+
setShowTopProgress(true);
|
|
13954
|
+
setHasInitialCloudResolved(false);
|
|
13950
13955
|
return;
|
|
13951
|
-
|
|
13952
13956
|
}
|
|
13953
13957
|
|
|
13954
13958
|
|
|
@@ -14245,7 +14249,7 @@ export function useTenantBootstrap({
|
|
|
14245
14249
|
|
|
14246
14250
|
isCloudMode,
|
|
14247
14251
|
|
|
14248
|
-
|
|
14252
|
+
bootSource,
|
|
14249
14253
|
|
|
14250
14254
|
useRenderBootstrap,
|
|
14251
14255
|
|
|
@@ -14291,9 +14295,7 @@ export function useTenantBootstrap({
|
|
|
14291
14295
|
|
|
14292
14296
|
isCloudMode,
|
|
14293
14297
|
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
isHotSaveMode,
|
|
14298
|
+
bootSource,
|
|
14297
14299
|
|
|
14298
14300
|
contentMode,
|
|
14299
14301
|
|
|
@@ -14307,6 +14309,8 @@ export function useTenantBootstrap({
|
|
|
14307
14309
|
|
|
14308
14310
|
isTenantEmpty,
|
|
14309
14311
|
|
|
14312
|
+
markCloudContentReady,
|
|
14313
|
+
|
|
14310
14314
|
retryBootstrap,
|
|
14311
14315
|
|
|
14312
14316
|
};
|