@olonjs/cli 3.0.150 → 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 +155 -149
- package/assets/templates/alpha/src_tenant.sh +155 -149
- 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,9 +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;
|
|
3088
|
+
|
|
3064
3089
|
const engineRefDocuments = useMemo(
|
|
3065
3090
|
() => ({
|
|
3066
3091
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3069,7 +3094,6 @@ function App() {
|
|
|
3069
3094
|
}),
|
|
3070
3095
|
[bootstrap.menuConfig],
|
|
3071
3096
|
);
|
|
3072
|
-
|
|
3073
3097
|
const config: JsonPagesConfig = {
|
|
3074
3098
|
tenantId: TENANT_ID,
|
|
3075
3099
|
basePath: APP_BASE_PATH,
|
|
@@ -3108,48 +3132,13 @@ function App() {
|
|
|
3108
3132
|
setCollections: bootstrap.setCollections,
|
|
3109
3133
|
});
|
|
3110
3134
|
},
|
|
3111
|
-
|
|
3112
|
-
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3113
|
-
throw new Error('Cloud mode is not configured for hot save.');
|
|
3114
|
-
}
|
|
3115
|
-
const apiBase = CLOUD_API_URL.replace(/\/$/, '');
|
|
3116
|
-
const res = await fetch(`${apiBase}/hotSave`, {
|
|
3117
|
-
method: 'POST',
|
|
3118
|
-
headers: {
|
|
3119
|
-
'Content-Type': 'application/json',
|
|
3120
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3121
|
-
},
|
|
3122
|
-
body: JSON.stringify({
|
|
3123
|
-
slug,
|
|
3124
|
-
page: state.page,
|
|
3125
|
-
siteConfig: state.site,
|
|
3126
|
-
collections: state.collections,
|
|
3127
|
-
}),
|
|
3128
|
-
});
|
|
3129
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
3130
|
-
if (!res.ok) {
|
|
3131
|
-
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
3132
|
-
}
|
|
3133
|
-
const keyFingerprint = cloudFingerprintFromUrl(CLOUD_API_URL, CLOUD_API_KEY);
|
|
3134
|
-
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3135
|
-
const existing = readCachedCloudContent(keyFingerprint);
|
|
3136
|
-
writeCachedCloudContent({
|
|
3137
|
-
keyFingerprint,
|
|
3138
|
-
savedAt: Date.now(),
|
|
3139
|
-
siteConfig: state.site ?? null,
|
|
3140
|
-
collections: state.collections,
|
|
3141
|
-
pages: {
|
|
3142
|
-
...(existing?.pages ?? {}),
|
|
3143
|
-
[normalizedSlug]: state.page,
|
|
3144
|
-
},
|
|
3145
|
-
});
|
|
3146
|
-
},
|
|
3135
|
+
hotSave,
|
|
3147
3136
|
async coldSave(state: ProjectState, slug: string): Promise<void> {
|
|
3148
3137
|
await runCloudSave({ state, slug }, true);
|
|
3149
3138
|
},
|
|
3150
|
-
showLocalSave:
|
|
3151
|
-
showHotSave:
|
|
3152
|
-
showColdSave:
|
|
3139
|
+
showLocalSave: cloudPolicy.showLocalSave,
|
|
3140
|
+
showHotSave: cloudPolicy.showHotSave,
|
|
3141
|
+
showColdSave: cloudPolicy.showColdSave,
|
|
3153
3142
|
},
|
|
3154
3143
|
assets: {
|
|
3155
3144
|
assetsBaseUrl: withBasePath('/assets', APP_BASE_PATH),
|
|
@@ -3157,9 +3146,9 @@ function App() {
|
|
|
3157
3146
|
onAssetUpload: (file) =>
|
|
3158
3147
|
uploadTenantAsset(file, {
|
|
3159
3148
|
basePath: APP_BASE_PATH,
|
|
3160
|
-
isCloudMode:
|
|
3161
|
-
cloudApiUrl:
|
|
3162
|
-
cloudApiKey:
|
|
3149
|
+
isCloudMode: cloudPolicy.isCloudMode,
|
|
3150
|
+
cloudApiUrl: cloudPolicy.apiUrl,
|
|
3151
|
+
cloudApiKey: cloudPolicy.apiKey,
|
|
3163
3152
|
apiBases: cloudApiCandidates,
|
|
3164
3153
|
onUploaded: loadAssetsManifest,
|
|
3165
3154
|
}),
|
|
@@ -3170,7 +3159,7 @@ function App() {
|
|
|
3170
3159
|
<ThemeProvider>
|
|
3171
3160
|
<OlonFormsContext.Provider value={formStates}>
|
|
3172
3161
|
<TenantBootstrapChrome
|
|
3173
|
-
isCloudMode={
|
|
3162
|
+
isCloudMode={cloudPolicy.isCloudMode}
|
|
3174
3163
|
showTopProgress={bootstrap.showTopProgress || (bootstrap.shouldRenderEngine && !fontsReady)}
|
|
3175
3164
|
contentMode={bootstrap.contentMode}
|
|
3176
3165
|
contentFallback={bootstrap.contentFallback}
|
|
@@ -4087,15 +4076,15 @@ echo "Creating src/components/form-demo/View.tsx..."
|
|
|
4087
4076
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
4088
4077
|
import { Icon } from '@/lib/IconResolver';
|
|
4089
4078
|
import { useFormState } from '@olonjs/react';
|
|
4079
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
4090
4080
|
import type { FormDemoData } from './types';
|
|
4091
4081
|
|
|
4092
4082
|
type FormDemoViewProps = {
|
|
4093
4083
|
data: FormDemoData;
|
|
4094
4084
|
};
|
|
4095
4085
|
|
|
4096
|
-
const
|
|
4097
|
-
|
|
4098
|
-
!import.meta.env.VITE_OLONJS_CLOUD_URL;
|
|
4086
|
+
const missingCloudUrl = !cloudPolicy.apiUrl;
|
|
4087
|
+
const hasCloudKey = Boolean(cloudPolicy.apiKey);
|
|
4099
4088
|
|
|
4100
4089
|
function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
4101
4090
|
const steps = [
|
|
@@ -4105,14 +4094,14 @@ function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
|
4105
4094
|
code: '"recipientEmail": "tu@esempio.it"',
|
|
4106
4095
|
},
|
|
4107
4096
|
{
|
|
4108
|
-
done: !
|
|
4109
|
-
label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
|
|
4110
|
-
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',
|
|
4111
4100
|
},
|
|
4112
4101
|
{
|
|
4113
|
-
done:
|
|
4114
|
-
label: 'VITE_JSONPAGES_API_KEY nel file .env',
|
|
4115
|
-
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-...',
|
|
4116
4105
|
},
|
|
4117
4106
|
];
|
|
4118
4107
|
|
|
@@ -11608,9 +11597,8 @@ echo "Creating src/lib/cloud/useAdminStudioContent.ts..."
|
|
|
11608
11597
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloud/useAdminStudioContent.ts"
|
|
11609
11598
|
import { useEffect, useRef } from 'react';
|
|
11610
11599
|
import type { Dispatch, SetStateAction } from 'react';
|
|
11611
|
-
import type { JsonPagesConfig } from '@olonjs/core';
|
|
11612
11600
|
import { applyLegacyCloudPayload, fetchLegacyCloudContentPayload } from '@/lib/cloud/cloudContentClient';
|
|
11613
|
-
import { cloudFingerprint, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11601
|
+
import { cloudFingerprint, readCachedPages, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11614
11602
|
import { isAdminPath, patchHistoryNavigation } from '@/lib/spp';
|
|
11615
11603
|
import { APP_BASE_PATH } from '@/lib/tenantEnv';
|
|
11616
11604
|
import type { PageConfig, SiteConfig } from '@/types';
|
|
@@ -11623,7 +11611,8 @@ type UseAdminStudioContentOptions = {
|
|
|
11623
11611
|
apiKey: string;
|
|
11624
11612
|
setPages: Dispatch<SetStateAction<Record<string, PageConfig>>>;
|
|
11625
11613
|
setSiteConfig: Dispatch<SetStateAction<SiteConfig>>;
|
|
11626
|
-
|
|
11614
|
+
/** Fired when /admin may paint — after cache hit and/or network attempt settles. */
|
|
11615
|
+
onSettled?: () => void;
|
|
11627
11616
|
};
|
|
11628
11617
|
|
|
11629
11618
|
/** Studio `/admin` sync via legacy `/content` — never mixed into visitor `/render` bootstrap. */
|
|
@@ -11633,13 +11622,28 @@ export function useAdminStudioContent({
|
|
|
11633
11622
|
apiKey,
|
|
11634
11623
|
setPages,
|
|
11635
11624
|
setSiteConfig,
|
|
11636
|
-
|
|
11625
|
+
onSettled,
|
|
11637
11626
|
}: UseAdminStudioContentOptions) {
|
|
11638
11627
|
const loadedRef = useRef(false);
|
|
11639
11628
|
const inFlightRef = useRef<Promise<void> | null>(null);
|
|
11629
|
+
const settledRef = useRef(false);
|
|
11640
11630
|
|
|
11641
11631
|
useEffect(() => {
|
|
11642
|
-
|
|
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
|
+
}
|
|
11643
11647
|
|
|
11644
11648
|
const syncIfAdmin = () => {
|
|
11645
11649
|
if (!isAdminPath(window.location.pathname, APP_BASE_PATH)) return;
|
|
@@ -11648,6 +11652,14 @@ export function useAdminStudioContent({
|
|
|
11648
11652
|
const controller = new AbortController();
|
|
11649
11653
|
const fingerprint = cloudFingerprint(apiCandidates[0]!, apiKey);
|
|
11650
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
|
+
|
|
11651
11663
|
inFlightRef.current = fetchLegacyCloudContentPayload(
|
|
11652
11664
|
apiCandidates,
|
|
11653
11665
|
apiKey,
|
|
@@ -11674,6 +11686,7 @@ export function useAdminStudioContent({
|
|
|
11674
11686
|
})
|
|
11675
11687
|
.finally(() => {
|
|
11676
11688
|
inFlightRef.current = null;
|
|
11689
|
+
settle();
|
|
11677
11690
|
});
|
|
11678
11691
|
};
|
|
11679
11692
|
|
|
@@ -11683,7 +11696,7 @@ export function useAdminStudioContent({
|
|
|
11683
11696
|
unpatch();
|
|
11684
11697
|
inFlightRef.current = null;
|
|
11685
11698
|
};
|
|
11686
|
-
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig,
|
|
11699
|
+
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig, onSettled]);
|
|
11687
11700
|
}
|
|
11688
11701
|
|
|
11689
11702
|
END_OF_FILE_CONTENT
|
|
@@ -11891,40 +11904,31 @@ END_OF_FILE_CONTENT
|
|
|
11891
11904
|
mkdir -p "src/lib/spp"
|
|
11892
11905
|
echo "Creating src/lib/spp/cloudConfig.ts..."
|
|
11893
11906
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/spp/cloudConfig.ts"
|
|
11894
|
-
|
|
11895
|
-
return raw.trim().replace(/\/+$/, '');
|
|
11896
|
-
}
|
|
11907
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
11897
11908
|
|
|
11898
|
-
|
|
11899
|
-
const base = normalizeApiBase(raw);
|
|
11900
|
-
const withApi = /\/api\/v1$/i.test(base) ? base : `${base}/api/v1`;
|
|
11901
|
-
return Array.from(new Set([withApi, base].filter(Boolean)));
|
|
11902
|
-
}
|
|
11909
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
11903
11910
|
|
|
11911
|
+
export { buildApiCandidates };
|
|
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
|
+
*/
|
|
11904
11919
|
export function getSppCloudConfig(): {
|
|
11905
11920
|
enabled: boolean;
|
|
11906
11921
|
apiBases: string[];
|
|
11907
11922
|
apiKey: string;
|
|
11908
11923
|
} {
|
|
11909
|
-
|
|
11910
|
-
import.meta.env.VITE_OLONJS_CLOUD_URL?.trim() ||
|
|
11911
|
-
import.meta.env.VITE_JSONPAGES_CLOUD_URL?.trim() ||
|
|
11912
|
-
'';
|
|
11913
|
-
const apiKey =
|
|
11914
|
-
import.meta.env.VITE_OLONJS_API_KEY?.trim() ||
|
|
11915
|
-
import.meta.env.VITE_JSONPAGES_API_KEY?.trim() ||
|
|
11916
|
-
'';
|
|
11917
|
-
const save2Repo = import.meta.env.VITE_SAVE2REPO === 'true';
|
|
11918
|
-
|
|
11919
|
-
// SSG/bake: local resolved JSON only. Cloud slices are browser-runtime (SPP §3).
|
|
11920
|
-
if (import.meta.env.SSR || !apiUrl || !apiKey || save2Repo) {
|
|
11924
|
+
if (import.meta.env.SSR || !cloudPolicy.isCloudMode) {
|
|
11921
11925
|
return { enabled: false, apiBases: [], apiKey: '' };
|
|
11922
11926
|
}
|
|
11923
11927
|
|
|
11924
11928
|
return {
|
|
11925
11929
|
enabled: true,
|
|
11926
|
-
apiBases: buildApiCandidates(apiUrl),
|
|
11927
|
-
apiKey,
|
|
11930
|
+
apiBases: buildApiCandidates(cloudPolicy.apiUrl),
|
|
11931
|
+
apiKey: cloudPolicy.apiKey,
|
|
11928
11932
|
};
|
|
11929
11933
|
}
|
|
11930
11934
|
|
|
@@ -13090,12 +13094,16 @@ END_OF_FILE_CONTENT
|
|
|
13090
13094
|
echo "Creating src/lib/tenantEnv.ts..."
|
|
13091
13095
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/tenantEnv.ts"
|
|
13092
13096
|
import { normalizeBasePath } from '@olonjs/core';
|
|
13097
|
+
import { readCloudEnvFromVite, resolveCloudPolicy, type CloudPolicy } from '@olonjs/react';
|
|
13098
|
+
|
|
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
|
+
);
|
|
13093
13103
|
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
export const CLOUD_API_KEY =
|
|
13097
|
-
import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
|
|
13098
|
-
export const SAVE2REPO_ENABLED = import.meta.env.VITE_SAVE2REPO === 'true';
|
|
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;
|
|
13099
13107
|
export const APP_BASE_PATH = normalizeBasePath(import.meta.env.BASE_URL || '/');
|
|
13100
13108
|
export const TENANT_ID = 'alpha';
|
|
13101
13109
|
|
|
@@ -13104,28 +13112,33 @@ echo "Creating src/lib/useAssetsManifest.ts..."
|
|
|
13104
13112
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useAssetsManifest.ts"
|
|
13105
13113
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
13106
13114
|
import type { LibraryImageEntry } from '@olonjs/core';
|
|
13107
|
-
import { buildApiCandidates } from '
|
|
13108
|
-
|
|
13115
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
13116
|
+
|
|
13117
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13109
13118
|
|
|
13110
13119
|
function normalizeApiBase(raw: string): string {
|
|
13111
13120
|
return raw.trim().replace(/\/+$/, '');
|
|
13112
13121
|
}
|
|
13113
13122
|
|
|
13114
|
-
|
|
13123
|
+
/** Asset library — cloud list when `cloudPolicy.isCloudMode`, else local `/api/list-assets`. */
|
|
13124
|
+
export function useAssetsManifest(isCloudMode: boolean = cloudPolicy.isCloudMode) {
|
|
13115
13125
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
13116
13126
|
const cloudApiCandidates = useMemo(
|
|
13117
|
-
() => (isCloudMode &&
|
|
13127
|
+
() => (isCloudMode && cloudPolicy.apiUrl ? buildApiCandidates(cloudPolicy.apiUrl) : []),
|
|
13118
13128
|
[isCloudMode],
|
|
13119
13129
|
);
|
|
13120
13130
|
|
|
13121
13131
|
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
13122
|
-
if (isCloudMode &&
|
|
13123
|
-
const apiBases =
|
|
13132
|
+
if (isCloudMode && cloudPolicy.apiUrl && cloudPolicy.apiKey) {
|
|
13133
|
+
const apiBases =
|
|
13134
|
+
cloudApiCandidates.length > 0
|
|
13135
|
+
? cloudApiCandidates
|
|
13136
|
+
: [normalizeApiBase(cloudPolicy.apiUrl)];
|
|
13124
13137
|
for (const apiBase of apiBases) {
|
|
13125
13138
|
try {
|
|
13126
13139
|
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
13127
13140
|
method: 'GET',
|
|
13128
|
-
headers: { Authorization: `Bearer ${
|
|
13141
|
+
headers: { Authorization: `Bearer ${cloudPolicy.apiKey}` },
|
|
13129
13142
|
});
|
|
13130
13143
|
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
13131
13144
|
if (!res.ok) continue;
|
|
@@ -13399,6 +13412,8 @@ echo "Creating src/lib/useFormSubmit.ts..."
|
|
|
13399
13412
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useFormSubmit.ts"
|
|
13400
13413
|
import { useState, useCallback } from 'react';
|
|
13401
13414
|
|
|
13415
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13416
|
+
|
|
13402
13417
|
export type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
|
|
13403
13418
|
|
|
13404
13419
|
interface UseFormSubmitOptions {
|
|
@@ -13411,22 +13426,20 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13411
13426
|
const [message, setMessage] = useState<string>('');
|
|
13412
13427
|
|
|
13413
13428
|
const submit = useCallback(async (
|
|
13414
|
-
formData: FormData,
|
|
13415
|
-
recipientEmail: string,
|
|
13416
|
-
pageSlug: string,
|
|
13429
|
+
formData: FormData,
|
|
13430
|
+
recipientEmail: string,
|
|
13431
|
+
pageSlug: string,
|
|
13417
13432
|
sectionId: string
|
|
13418
13433
|
) => {
|
|
13419
|
-
const
|
|
13420
|
-
const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
|
|
13434
|
+
const { apiUrl, apiKey } = cloudPolicy;
|
|
13421
13435
|
|
|
13422
|
-
if (!
|
|
13436
|
+
if (!apiUrl || !apiKey) {
|
|
13423
13437
|
setStatus('error');
|
|
13424
13438
|
setMessage('Configurazione API non disponibile. Riprova tra poco.');
|
|
13425
13439
|
return false;
|
|
13426
13440
|
}
|
|
13427
13441
|
|
|
13428
|
-
|
|
13429
|
-
const data: Record<string, any> = {};
|
|
13442
|
+
const data: Record<string, string> = {};
|
|
13430
13443
|
formData.forEach((value, key) => {
|
|
13431
13444
|
data[key] = String(value).trim();
|
|
13432
13445
|
});
|
|
@@ -13441,18 +13454,17 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13441
13454
|
submittedAt: new Date().toISOString(),
|
|
13442
13455
|
};
|
|
13443
13456
|
|
|
13444
|
-
// Idempotency Key per evitare doppi invii accidentali
|
|
13445
13457
|
const idempotencyKey = `form-${sectionId}-${Date.now()}`;
|
|
13446
13458
|
|
|
13447
13459
|
setStatus('submitting');
|
|
13448
13460
|
setMessage('Invio in corso...');
|
|
13449
13461
|
|
|
13450
13462
|
try {
|
|
13451
|
-
const apiBase =
|
|
13463
|
+
const apiBase = apiUrl.replace(/\/$/, '');
|
|
13452
13464
|
const response = await fetch(`${apiBase}/forms/submit`, {
|
|
13453
13465
|
method: 'POST',
|
|
13454
13466
|
headers: {
|
|
13455
|
-
Authorization: `Bearer ${
|
|
13467
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13456
13468
|
'Content-Type': 'application/json',
|
|
13457
13469
|
'Idempotency-Key': idempotencyKey,
|
|
13458
13470
|
},
|
|
@@ -13482,23 +13494,18 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13482
13494
|
}, []);
|
|
13483
13495
|
|
|
13484
13496
|
return { submit, status, message, reset };
|
|
13485
|
-
}
|
|
13497
|
+
}
|
|
13498
|
+
|
|
13486
13499
|
END_OF_FILE_CONTENT
|
|
13487
13500
|
echo "Creating src/lib/useOlonForms.ts..."
|
|
13488
13501
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
13489
13502
|
import { useCallback, useEffect, useState } from 'react';
|
|
13490
13503
|
import type { FormState } from '@olonjs/react';
|
|
13491
13504
|
|
|
13492
|
-
|
|
13493
|
-
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
13494
|
-
(import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
|
|
13495
|
-
|
|
13496
|
-
const API_KEY =
|
|
13497
|
-
(import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
|
|
13498
|
-
(import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
|
|
13505
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13499
13506
|
|
|
13500
13507
|
interface UseOlonFormsOptions {
|
|
13501
|
-
/** Override the submit endpoint. Defaults to
|
|
13508
|
+
/** Override the submit endpoint. Defaults to cloudPolicy.apiUrl/forms/submit */
|
|
13502
13509
|
endpoint?: string;
|
|
13503
13510
|
}
|
|
13504
13511
|
|
|
@@ -13517,13 +13524,16 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13517
13524
|
}, []);
|
|
13518
13525
|
|
|
13519
13526
|
useEffect(() => {
|
|
13527
|
+
const apiUrl = cloudPolicy.apiUrl;
|
|
13528
|
+
const apiKey = cloudPolicy.apiKey;
|
|
13529
|
+
|
|
13520
13530
|
const resolvedBase = options?.endpoint
|
|
13521
13531
|
? options.endpoint.replace(/\/$/, '')
|
|
13522
|
-
:
|
|
13523
|
-
?
|
|
13532
|
+
: apiUrl
|
|
13533
|
+
? apiUrl.replace(/\/$/, '')
|
|
13524
13534
|
: null;
|
|
13525
13535
|
|
|
13526
|
-
if (!resolvedBase || !
|
|
13536
|
+
if (!resolvedBase || !apiKey) {
|
|
13527
13537
|
console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
|
|
13528
13538
|
return;
|
|
13529
13539
|
}
|
|
@@ -13566,7 +13576,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13566
13576
|
const response = await fetch(endpoint, {
|
|
13567
13577
|
method: 'POST',
|
|
13568
13578
|
headers: {
|
|
13569
|
-
Authorization: `Bearer ${
|
|
13579
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13570
13580
|
'Content-Type': 'application/json',
|
|
13571
13581
|
'Idempotency-Key': `form-${formId}-${Date.now()}`,
|
|
13572
13582
|
},
|
|
@@ -13617,7 +13627,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13617
13627
|
END_OF_FILE_CONTENT
|
|
13618
13628
|
echo "Creating src/lib/useTenantBootstrap.ts..."
|
|
13619
13629
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useTenantBootstrap.ts"
|
|
13620
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13630
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13621
13631
|
|
|
13622
13632
|
import { logBootstrapEvent, toCloudLoadFailure } from '@/lib/cloud/bootstrapTelemetry';
|
|
13623
13633
|
|
|
@@ -13647,7 +13657,7 @@ import {
|
|
|
13647
13657
|
|
|
13648
13658
|
import type { JsonPagesConfig } from '@olonjs/core';
|
|
13649
13659
|
|
|
13650
|
-
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';
|
|
13651
13661
|
|
|
13652
13662
|
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
13653
13663
|
|
|
@@ -13729,13 +13739,10 @@ export function useTenantBootstrap({
|
|
|
13729
13739
|
|
|
13730
13740
|
}: UseTenantBootstrapOptions) {
|
|
13731
13741
|
|
|
13732
|
-
const isCloudMode
|
|
13733
|
-
|
|
13734
|
-
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
13742
|
+
const { isCloudMode, bootSource } = cloudPolicy;
|
|
13735
13743
|
|
|
13736
|
-
|
|
13737
|
-
|
|
13738
|
-
const useRenderBootstrap = isHotSaveMode;
|
|
13744
|
+
/** Live SPP render boot — only when bootSource is live (not static Save2Repo). */
|
|
13745
|
+
const useRenderBootstrap = bootSource === 'live';
|
|
13739
13746
|
|
|
13740
13747
|
|
|
13741
13748
|
|
|
@@ -13799,7 +13806,11 @@ export function useTenantBootstrap({
|
|
|
13799
13806
|
|
|
13800
13807
|
const isTenantEmpty = Object.keys(pages).length === 0;
|
|
13801
13808
|
|
|
13802
|
-
|
|
13809
|
+
/** Call when live /admin content sync has finished (success, cache, or error). */
|
|
13810
|
+
const markCloudContentReady = useCallback(() => {
|
|
13811
|
+
setShowTopProgress(false);
|
|
13812
|
+
setHasInitialCloudResolved(true);
|
|
13813
|
+
}, []);
|
|
13803
13814
|
|
|
13804
13815
|
const retryBootstrap = () => {
|
|
13805
13816
|
|
|
@@ -13839,7 +13850,7 @@ export function useTenantBootstrap({
|
|
|
13839
13850
|
|
|
13840
13851
|
|
|
13841
13852
|
|
|
13842
|
-
if (
|
|
13853
|
+
if (bootSource === 'static') {
|
|
13843
13854
|
|
|
13844
13855
|
if (contentLoadInFlight.current) return;
|
|
13845
13856
|
|
|
@@ -13936,17 +13947,12 @@ export function useTenantBootstrap({
|
|
|
13936
13947
|
|
|
13937
13948
|
|
|
13938
13949
|
if (isAdminPath(window.location.pathname, APP_BASE_PATH)) {
|
|
13939
|
-
|
|
13950
|
+
// Live /admin: content comes from useAdminStudioContent — do not paint empty first.
|
|
13940
13951
|
setContentMode('cloud');
|
|
13941
|
-
|
|
13942
13952
|
setContentFallback(null);
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
setHasInitialCloudResolved(true);
|
|
13947
|
-
|
|
13953
|
+
setShowTopProgress(true);
|
|
13954
|
+
setHasInitialCloudResolved(false);
|
|
13948
13955
|
return;
|
|
13949
|
-
|
|
13950
13956
|
}
|
|
13951
13957
|
|
|
13952
13958
|
|
|
@@ -14243,7 +14249,7 @@ export function useTenantBootstrap({
|
|
|
14243
14249
|
|
|
14244
14250
|
isCloudMode,
|
|
14245
14251
|
|
|
14246
|
-
|
|
14252
|
+
bootSource,
|
|
14247
14253
|
|
|
14248
14254
|
useRenderBootstrap,
|
|
14249
14255
|
|
|
@@ -14289,9 +14295,7 @@ export function useTenantBootstrap({
|
|
|
14289
14295
|
|
|
14290
14296
|
isCloudMode,
|
|
14291
14297
|
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
isHotSaveMode,
|
|
14298
|
+
bootSource,
|
|
14295
14299
|
|
|
14296
14300
|
contentMode,
|
|
14297
14301
|
|
|
@@ -14305,6 +14309,8 @@ export function useTenantBootstrap({
|
|
|
14305
14309
|
|
|
14306
14310
|
isTenantEmpty,
|
|
14307
14311
|
|
|
14312
|
+
markCloudContentReady,
|
|
14313
|
+
|
|
14308
14314
|
retryBootstrap,
|
|
14309
14315
|
|
|
14310
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,9 +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;
|
|
3088
|
+
|
|
3064
3089
|
const engineRefDocuments = useMemo(
|
|
3065
3090
|
() => ({
|
|
3066
3091
|
'menu.json': bootstrap.menuConfig,
|
|
@@ -3069,7 +3094,6 @@ function App() {
|
|
|
3069
3094
|
}),
|
|
3070
3095
|
[bootstrap.menuConfig],
|
|
3071
3096
|
);
|
|
3072
|
-
|
|
3073
3097
|
const config: JsonPagesConfig = {
|
|
3074
3098
|
tenantId: TENANT_ID,
|
|
3075
3099
|
basePath: APP_BASE_PATH,
|
|
@@ -3108,48 +3132,13 @@ function App() {
|
|
|
3108
3132
|
setCollections: bootstrap.setCollections,
|
|
3109
3133
|
});
|
|
3110
3134
|
},
|
|
3111
|
-
|
|
3112
|
-
if (!bootstrap.isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
|
|
3113
|
-
throw new Error('Cloud mode is not configured for hot save.');
|
|
3114
|
-
}
|
|
3115
|
-
const apiBase = CLOUD_API_URL.replace(/\/$/, '');
|
|
3116
|
-
const res = await fetch(`${apiBase}/hotSave`, {
|
|
3117
|
-
method: 'POST',
|
|
3118
|
-
headers: {
|
|
3119
|
-
'Content-Type': 'application/json',
|
|
3120
|
-
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3121
|
-
},
|
|
3122
|
-
body: JSON.stringify({
|
|
3123
|
-
slug,
|
|
3124
|
-
page: state.page,
|
|
3125
|
-
siteConfig: state.site,
|
|
3126
|
-
collections: state.collections,
|
|
3127
|
-
}),
|
|
3128
|
-
});
|
|
3129
|
-
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
3130
|
-
if (!res.ok) {
|
|
3131
|
-
throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
|
|
3132
|
-
}
|
|
3133
|
-
const keyFingerprint = cloudFingerprintFromUrl(CLOUD_API_URL, CLOUD_API_KEY);
|
|
3134
|
-
const normalizedSlug = normalizeSlugForCache(slug);
|
|
3135
|
-
const existing = readCachedCloudContent(keyFingerprint);
|
|
3136
|
-
writeCachedCloudContent({
|
|
3137
|
-
keyFingerprint,
|
|
3138
|
-
savedAt: Date.now(),
|
|
3139
|
-
siteConfig: state.site ?? null,
|
|
3140
|
-
collections: state.collections,
|
|
3141
|
-
pages: {
|
|
3142
|
-
...(existing?.pages ?? {}),
|
|
3143
|
-
[normalizedSlug]: state.page,
|
|
3144
|
-
},
|
|
3145
|
-
});
|
|
3146
|
-
},
|
|
3135
|
+
hotSave,
|
|
3147
3136
|
async coldSave(state: ProjectState, slug: string): Promise<void> {
|
|
3148
3137
|
await runCloudSave({ state, slug }, true);
|
|
3149
3138
|
},
|
|
3150
|
-
showLocalSave:
|
|
3151
|
-
showHotSave:
|
|
3152
|
-
showColdSave:
|
|
3139
|
+
showLocalSave: cloudPolicy.showLocalSave,
|
|
3140
|
+
showHotSave: cloudPolicy.showHotSave,
|
|
3141
|
+
showColdSave: cloudPolicy.showColdSave,
|
|
3153
3142
|
},
|
|
3154
3143
|
assets: {
|
|
3155
3144
|
assetsBaseUrl: withBasePath('/assets', APP_BASE_PATH),
|
|
@@ -3157,9 +3146,9 @@ function App() {
|
|
|
3157
3146
|
onAssetUpload: (file) =>
|
|
3158
3147
|
uploadTenantAsset(file, {
|
|
3159
3148
|
basePath: APP_BASE_PATH,
|
|
3160
|
-
isCloudMode:
|
|
3161
|
-
cloudApiUrl:
|
|
3162
|
-
cloudApiKey:
|
|
3149
|
+
isCloudMode: cloudPolicy.isCloudMode,
|
|
3150
|
+
cloudApiUrl: cloudPolicy.apiUrl,
|
|
3151
|
+
cloudApiKey: cloudPolicy.apiKey,
|
|
3163
3152
|
apiBases: cloudApiCandidates,
|
|
3164
3153
|
onUploaded: loadAssetsManifest,
|
|
3165
3154
|
}),
|
|
@@ -3170,7 +3159,7 @@ function App() {
|
|
|
3170
3159
|
<ThemeProvider>
|
|
3171
3160
|
<OlonFormsContext.Provider value={formStates}>
|
|
3172
3161
|
<TenantBootstrapChrome
|
|
3173
|
-
isCloudMode={
|
|
3162
|
+
isCloudMode={cloudPolicy.isCloudMode}
|
|
3174
3163
|
showTopProgress={bootstrap.showTopProgress || (bootstrap.shouldRenderEngine && !fontsReady)}
|
|
3175
3164
|
contentMode={bootstrap.contentMode}
|
|
3176
3165
|
contentFallback={bootstrap.contentFallback}
|
|
@@ -4087,15 +4076,15 @@ echo "Creating src/components/form-demo/View.tsx..."
|
|
|
4087
4076
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
4088
4077
|
import { Icon } from '@/lib/IconResolver';
|
|
4089
4078
|
import { useFormState } from '@olonjs/react';
|
|
4079
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
4090
4080
|
import type { FormDemoData } from './types';
|
|
4091
4081
|
|
|
4092
4082
|
type FormDemoViewProps = {
|
|
4093
4083
|
data: FormDemoData;
|
|
4094
4084
|
};
|
|
4095
4085
|
|
|
4096
|
-
const
|
|
4097
|
-
|
|
4098
|
-
!import.meta.env.VITE_OLONJS_CLOUD_URL;
|
|
4086
|
+
const missingCloudUrl = !cloudPolicy.apiUrl;
|
|
4087
|
+
const hasCloudKey = Boolean(cloudPolicy.apiKey);
|
|
4099
4088
|
|
|
4100
4089
|
function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
4101
4090
|
const steps = [
|
|
@@ -4105,14 +4094,14 @@ function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
|
4105
4094
|
code: '"recipientEmail": "tu@esempio.it"',
|
|
4106
4095
|
},
|
|
4107
4096
|
{
|
|
4108
|
-
done: !
|
|
4109
|
-
label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
|
|
4110
|
-
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',
|
|
4111
4100
|
},
|
|
4112
4101
|
{
|
|
4113
|
-
done:
|
|
4114
|
-
label: 'VITE_JSONPAGES_API_KEY nel file .env',
|
|
4115
|
-
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-...',
|
|
4116
4105
|
},
|
|
4117
4106
|
];
|
|
4118
4107
|
|
|
@@ -11608,9 +11597,8 @@ echo "Creating src/lib/cloud/useAdminStudioContent.ts..."
|
|
|
11608
11597
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloud/useAdminStudioContent.ts"
|
|
11609
11598
|
import { useEffect, useRef } from 'react';
|
|
11610
11599
|
import type { Dispatch, SetStateAction } from 'react';
|
|
11611
|
-
import type { JsonPagesConfig } from '@olonjs/core';
|
|
11612
11600
|
import { applyLegacyCloudPayload, fetchLegacyCloudContentPayload } from '@/lib/cloud/cloudContentClient';
|
|
11613
|
-
import { cloudFingerprint, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11601
|
+
import { cloudFingerprint, readCachedPages, writeCachedCloudContent } from '@/lib/cloud/cloudCache';
|
|
11614
11602
|
import { isAdminPath, patchHistoryNavigation } from '@/lib/spp';
|
|
11615
11603
|
import { APP_BASE_PATH } from '@/lib/tenantEnv';
|
|
11616
11604
|
import type { PageConfig, SiteConfig } from '@/types';
|
|
@@ -11623,7 +11611,8 @@ type UseAdminStudioContentOptions = {
|
|
|
11623
11611
|
apiKey: string;
|
|
11624
11612
|
setPages: Dispatch<SetStateAction<Record<string, PageConfig>>>;
|
|
11625
11613
|
setSiteConfig: Dispatch<SetStateAction<SiteConfig>>;
|
|
11626
|
-
|
|
11614
|
+
/** Fired when /admin may paint — after cache hit and/or network attempt settles. */
|
|
11615
|
+
onSettled?: () => void;
|
|
11627
11616
|
};
|
|
11628
11617
|
|
|
11629
11618
|
/** Studio `/admin` sync via legacy `/content` — never mixed into visitor `/render` bootstrap. */
|
|
@@ -11633,13 +11622,28 @@ export function useAdminStudioContent({
|
|
|
11633
11622
|
apiKey,
|
|
11634
11623
|
setPages,
|
|
11635
11624
|
setSiteConfig,
|
|
11636
|
-
|
|
11625
|
+
onSettled,
|
|
11637
11626
|
}: UseAdminStudioContentOptions) {
|
|
11638
11627
|
const loadedRef = useRef(false);
|
|
11639
11628
|
const inFlightRef = useRef<Promise<void> | null>(null);
|
|
11629
|
+
const settledRef = useRef(false);
|
|
11640
11630
|
|
|
11641
11631
|
useEffect(() => {
|
|
11642
|
-
|
|
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
|
+
}
|
|
11643
11647
|
|
|
11644
11648
|
const syncIfAdmin = () => {
|
|
11645
11649
|
if (!isAdminPath(window.location.pathname, APP_BASE_PATH)) return;
|
|
@@ -11648,6 +11652,14 @@ export function useAdminStudioContent({
|
|
|
11648
11652
|
const controller = new AbortController();
|
|
11649
11653
|
const fingerprint = cloudFingerprint(apiCandidates[0]!, apiKey);
|
|
11650
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
|
+
|
|
11651
11663
|
inFlightRef.current = fetchLegacyCloudContentPayload(
|
|
11652
11664
|
apiCandidates,
|
|
11653
11665
|
apiKey,
|
|
@@ -11674,6 +11686,7 @@ export function useAdminStudioContent({
|
|
|
11674
11686
|
})
|
|
11675
11687
|
.finally(() => {
|
|
11676
11688
|
inFlightRef.current = null;
|
|
11689
|
+
settle();
|
|
11677
11690
|
});
|
|
11678
11691
|
};
|
|
11679
11692
|
|
|
@@ -11683,7 +11696,7 @@ export function useAdminStudioContent({
|
|
|
11683
11696
|
unpatch();
|
|
11684
11697
|
inFlightRef.current = null;
|
|
11685
11698
|
};
|
|
11686
|
-
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig,
|
|
11699
|
+
}, [enabled, apiCandidates, apiKey, setPages, setSiteConfig, onSettled]);
|
|
11687
11700
|
}
|
|
11688
11701
|
|
|
11689
11702
|
END_OF_FILE_CONTENT
|
|
@@ -11891,40 +11904,31 @@ END_OF_FILE_CONTENT
|
|
|
11891
11904
|
mkdir -p "src/lib/spp"
|
|
11892
11905
|
echo "Creating src/lib/spp/cloudConfig.ts..."
|
|
11893
11906
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/spp/cloudConfig.ts"
|
|
11894
|
-
|
|
11895
|
-
return raw.trim().replace(/\/+$/, '');
|
|
11896
|
-
}
|
|
11907
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
11897
11908
|
|
|
11898
|
-
|
|
11899
|
-
const base = normalizeApiBase(raw);
|
|
11900
|
-
const withApi = /\/api\/v1$/i.test(base) ? base : `${base}/api/v1`;
|
|
11901
|
-
return Array.from(new Set([withApi, base].filter(Boolean)));
|
|
11902
|
-
}
|
|
11909
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
11903
11910
|
|
|
11911
|
+
export { buildApiCandidates };
|
|
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
|
+
*/
|
|
11904
11919
|
export function getSppCloudConfig(): {
|
|
11905
11920
|
enabled: boolean;
|
|
11906
11921
|
apiBases: string[];
|
|
11907
11922
|
apiKey: string;
|
|
11908
11923
|
} {
|
|
11909
|
-
|
|
11910
|
-
import.meta.env.VITE_OLONJS_CLOUD_URL?.trim() ||
|
|
11911
|
-
import.meta.env.VITE_JSONPAGES_CLOUD_URL?.trim() ||
|
|
11912
|
-
'';
|
|
11913
|
-
const apiKey =
|
|
11914
|
-
import.meta.env.VITE_OLONJS_API_KEY?.trim() ||
|
|
11915
|
-
import.meta.env.VITE_JSONPAGES_API_KEY?.trim() ||
|
|
11916
|
-
'';
|
|
11917
|
-
const save2Repo = import.meta.env.VITE_SAVE2REPO === 'true';
|
|
11918
|
-
|
|
11919
|
-
// SSG/bake: local resolved JSON only. Cloud slices are browser-runtime (SPP §3).
|
|
11920
|
-
if (import.meta.env.SSR || !apiUrl || !apiKey || save2Repo) {
|
|
11924
|
+
if (import.meta.env.SSR || !cloudPolicy.isCloudMode) {
|
|
11921
11925
|
return { enabled: false, apiBases: [], apiKey: '' };
|
|
11922
11926
|
}
|
|
11923
11927
|
|
|
11924
11928
|
return {
|
|
11925
11929
|
enabled: true,
|
|
11926
|
-
apiBases: buildApiCandidates(apiUrl),
|
|
11927
|
-
apiKey,
|
|
11930
|
+
apiBases: buildApiCandidates(cloudPolicy.apiUrl),
|
|
11931
|
+
apiKey: cloudPolicy.apiKey,
|
|
11928
11932
|
};
|
|
11929
11933
|
}
|
|
11930
11934
|
|
|
@@ -13090,12 +13094,16 @@ END_OF_FILE_CONTENT
|
|
|
13090
13094
|
echo "Creating src/lib/tenantEnv.ts..."
|
|
13091
13095
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/tenantEnv.ts"
|
|
13092
13096
|
import { normalizeBasePath } from '@olonjs/core';
|
|
13097
|
+
import { readCloudEnvFromVite, resolveCloudPolicy, type CloudPolicy } from '@olonjs/react';
|
|
13098
|
+
|
|
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
|
+
);
|
|
13093
13103
|
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
export const CLOUD_API_KEY =
|
|
13097
|
-
import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
|
|
13098
|
-
export const SAVE2REPO_ENABLED = import.meta.env.VITE_SAVE2REPO === 'true';
|
|
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;
|
|
13099
13107
|
export const APP_BASE_PATH = normalizeBasePath(import.meta.env.BASE_URL || '/');
|
|
13100
13108
|
export const TENANT_ID = 'alpha';
|
|
13101
13109
|
|
|
@@ -13104,28 +13112,33 @@ echo "Creating src/lib/useAssetsManifest.ts..."
|
|
|
13104
13112
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useAssetsManifest.ts"
|
|
13105
13113
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
13106
13114
|
import type { LibraryImageEntry } from '@olonjs/core';
|
|
13107
|
-
import { buildApiCandidates } from '
|
|
13108
|
-
|
|
13115
|
+
import { buildApiCandidates } from '@olonjs/react';
|
|
13116
|
+
|
|
13117
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13109
13118
|
|
|
13110
13119
|
function normalizeApiBase(raw: string): string {
|
|
13111
13120
|
return raw.trim().replace(/\/+$/, '');
|
|
13112
13121
|
}
|
|
13113
13122
|
|
|
13114
|
-
|
|
13123
|
+
/** Asset library — cloud list when `cloudPolicy.isCloudMode`, else local `/api/list-assets`. */
|
|
13124
|
+
export function useAssetsManifest(isCloudMode: boolean = cloudPolicy.isCloudMode) {
|
|
13115
13125
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
13116
13126
|
const cloudApiCandidates = useMemo(
|
|
13117
|
-
() => (isCloudMode &&
|
|
13127
|
+
() => (isCloudMode && cloudPolicy.apiUrl ? buildApiCandidates(cloudPolicy.apiUrl) : []),
|
|
13118
13128
|
[isCloudMode],
|
|
13119
13129
|
);
|
|
13120
13130
|
|
|
13121
13131
|
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
13122
|
-
if (isCloudMode &&
|
|
13123
|
-
const apiBases =
|
|
13132
|
+
if (isCloudMode && cloudPolicy.apiUrl && cloudPolicy.apiKey) {
|
|
13133
|
+
const apiBases =
|
|
13134
|
+
cloudApiCandidates.length > 0
|
|
13135
|
+
? cloudApiCandidates
|
|
13136
|
+
: [normalizeApiBase(cloudPolicy.apiUrl)];
|
|
13124
13137
|
for (const apiBase of apiBases) {
|
|
13125
13138
|
try {
|
|
13126
13139
|
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
13127
13140
|
method: 'GET',
|
|
13128
|
-
headers: { Authorization: `Bearer ${
|
|
13141
|
+
headers: { Authorization: `Bearer ${cloudPolicy.apiKey}` },
|
|
13129
13142
|
});
|
|
13130
13143
|
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
13131
13144
|
if (!res.ok) continue;
|
|
@@ -13399,6 +13412,8 @@ echo "Creating src/lib/useFormSubmit.ts..."
|
|
|
13399
13412
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useFormSubmit.ts"
|
|
13400
13413
|
import { useState, useCallback } from 'react';
|
|
13401
13414
|
|
|
13415
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13416
|
+
|
|
13402
13417
|
export type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
|
|
13403
13418
|
|
|
13404
13419
|
interface UseFormSubmitOptions {
|
|
@@ -13411,22 +13426,20 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13411
13426
|
const [message, setMessage] = useState<string>('');
|
|
13412
13427
|
|
|
13413
13428
|
const submit = useCallback(async (
|
|
13414
|
-
formData: FormData,
|
|
13415
|
-
recipientEmail: string,
|
|
13416
|
-
pageSlug: string,
|
|
13429
|
+
formData: FormData,
|
|
13430
|
+
recipientEmail: string,
|
|
13431
|
+
pageSlug: string,
|
|
13417
13432
|
sectionId: string
|
|
13418
13433
|
) => {
|
|
13419
|
-
const
|
|
13420
|
-
const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
|
|
13434
|
+
const { apiUrl, apiKey } = cloudPolicy;
|
|
13421
13435
|
|
|
13422
|
-
if (!
|
|
13436
|
+
if (!apiUrl || !apiKey) {
|
|
13423
13437
|
setStatus('error');
|
|
13424
13438
|
setMessage('Configurazione API non disponibile. Riprova tra poco.');
|
|
13425
13439
|
return false;
|
|
13426
13440
|
}
|
|
13427
13441
|
|
|
13428
|
-
|
|
13429
|
-
const data: Record<string, any> = {};
|
|
13442
|
+
const data: Record<string, string> = {};
|
|
13430
13443
|
formData.forEach((value, key) => {
|
|
13431
13444
|
data[key] = String(value).trim();
|
|
13432
13445
|
});
|
|
@@ -13441,18 +13454,17 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13441
13454
|
submittedAt: new Date().toISOString(),
|
|
13442
13455
|
};
|
|
13443
13456
|
|
|
13444
|
-
// Idempotency Key per evitare doppi invii accidentali
|
|
13445
13457
|
const idempotencyKey = `form-${sectionId}-${Date.now()}`;
|
|
13446
13458
|
|
|
13447
13459
|
setStatus('submitting');
|
|
13448
13460
|
setMessage('Invio in corso...');
|
|
13449
13461
|
|
|
13450
13462
|
try {
|
|
13451
|
-
const apiBase =
|
|
13463
|
+
const apiBase = apiUrl.replace(/\/$/, '');
|
|
13452
13464
|
const response = await fetch(`${apiBase}/forms/submit`, {
|
|
13453
13465
|
method: 'POST',
|
|
13454
13466
|
headers: {
|
|
13455
|
-
Authorization: `Bearer ${
|
|
13467
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13456
13468
|
'Content-Type': 'application/json',
|
|
13457
13469
|
'Idempotency-Key': idempotencyKey,
|
|
13458
13470
|
},
|
|
@@ -13482,23 +13494,18 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
13482
13494
|
}, []);
|
|
13483
13495
|
|
|
13484
13496
|
return { submit, status, message, reset };
|
|
13485
|
-
}
|
|
13497
|
+
}
|
|
13498
|
+
|
|
13486
13499
|
END_OF_FILE_CONTENT
|
|
13487
13500
|
echo "Creating src/lib/useOlonForms.ts..."
|
|
13488
13501
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
13489
13502
|
import { useCallback, useEffect, useState } from 'react';
|
|
13490
13503
|
import type { FormState } from '@olonjs/react';
|
|
13491
13504
|
|
|
13492
|
-
|
|
13493
|
-
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
13494
|
-
(import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
|
|
13495
|
-
|
|
13496
|
-
const API_KEY =
|
|
13497
|
-
(import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
|
|
13498
|
-
(import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
|
|
13505
|
+
import { cloudPolicy } from '@/lib/tenantEnv';
|
|
13499
13506
|
|
|
13500
13507
|
interface UseOlonFormsOptions {
|
|
13501
|
-
/** Override the submit endpoint. Defaults to
|
|
13508
|
+
/** Override the submit endpoint. Defaults to cloudPolicy.apiUrl/forms/submit */
|
|
13502
13509
|
endpoint?: string;
|
|
13503
13510
|
}
|
|
13504
13511
|
|
|
@@ -13517,13 +13524,16 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13517
13524
|
}, []);
|
|
13518
13525
|
|
|
13519
13526
|
useEffect(() => {
|
|
13527
|
+
const apiUrl = cloudPolicy.apiUrl;
|
|
13528
|
+
const apiKey = cloudPolicy.apiKey;
|
|
13529
|
+
|
|
13520
13530
|
const resolvedBase = options?.endpoint
|
|
13521
13531
|
? options.endpoint.replace(/\/$/, '')
|
|
13522
|
-
:
|
|
13523
|
-
?
|
|
13532
|
+
: apiUrl
|
|
13533
|
+
? apiUrl.replace(/\/$/, '')
|
|
13524
13534
|
: null;
|
|
13525
13535
|
|
|
13526
|
-
if (!resolvedBase || !
|
|
13536
|
+
if (!resolvedBase || !apiKey) {
|
|
13527
13537
|
console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
|
|
13528
13538
|
return;
|
|
13529
13539
|
}
|
|
@@ -13566,7 +13576,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13566
13576
|
const response = await fetch(endpoint, {
|
|
13567
13577
|
method: 'POST',
|
|
13568
13578
|
headers: {
|
|
13569
|
-
Authorization: `Bearer ${
|
|
13579
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13570
13580
|
'Content-Type': 'application/json',
|
|
13571
13581
|
'Idempotency-Key': `form-${formId}-${Date.now()}`,
|
|
13572
13582
|
},
|
|
@@ -13617,7 +13627,7 @@ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<st
|
|
|
13617
13627
|
END_OF_FILE_CONTENT
|
|
13618
13628
|
echo "Creating src/lib/useTenantBootstrap.ts..."
|
|
13619
13629
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/useTenantBootstrap.ts"
|
|
13620
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13630
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13621
13631
|
|
|
13622
13632
|
import { logBootstrapEvent, toCloudLoadFailure } from '@/lib/cloud/bootstrapTelemetry';
|
|
13623
13633
|
|
|
@@ -13647,7 +13657,7 @@ import {
|
|
|
13647
13657
|
|
|
13648
13658
|
import type { JsonPagesConfig } from '@olonjs/core';
|
|
13649
13659
|
|
|
13650
|
-
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';
|
|
13651
13661
|
|
|
13652
13662
|
import type { MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
13653
13663
|
|
|
@@ -13729,13 +13739,10 @@ export function useTenantBootstrap({
|
|
|
13729
13739
|
|
|
13730
13740
|
}: UseTenantBootstrapOptions) {
|
|
13731
13741
|
|
|
13732
|
-
const isCloudMode
|
|
13733
|
-
|
|
13734
|
-
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
13742
|
+
const { isCloudMode, bootSource } = cloudPolicy;
|
|
13735
13743
|
|
|
13736
|
-
|
|
13737
|
-
|
|
13738
|
-
const useRenderBootstrap = isHotSaveMode;
|
|
13744
|
+
/** Live SPP render boot — only when bootSource is live (not static Save2Repo). */
|
|
13745
|
+
const useRenderBootstrap = bootSource === 'live';
|
|
13739
13746
|
|
|
13740
13747
|
|
|
13741
13748
|
|
|
@@ -13799,7 +13806,11 @@ export function useTenantBootstrap({
|
|
|
13799
13806
|
|
|
13800
13807
|
const isTenantEmpty = Object.keys(pages).length === 0;
|
|
13801
13808
|
|
|
13802
|
-
|
|
13809
|
+
/** Call when live /admin content sync has finished (success, cache, or error). */
|
|
13810
|
+
const markCloudContentReady = useCallback(() => {
|
|
13811
|
+
setShowTopProgress(false);
|
|
13812
|
+
setHasInitialCloudResolved(true);
|
|
13813
|
+
}, []);
|
|
13803
13814
|
|
|
13804
13815
|
const retryBootstrap = () => {
|
|
13805
13816
|
|
|
@@ -13839,7 +13850,7 @@ export function useTenantBootstrap({
|
|
|
13839
13850
|
|
|
13840
13851
|
|
|
13841
13852
|
|
|
13842
|
-
if (
|
|
13853
|
+
if (bootSource === 'static') {
|
|
13843
13854
|
|
|
13844
13855
|
if (contentLoadInFlight.current) return;
|
|
13845
13856
|
|
|
@@ -13936,17 +13947,12 @@ export function useTenantBootstrap({
|
|
|
13936
13947
|
|
|
13937
13948
|
|
|
13938
13949
|
if (isAdminPath(window.location.pathname, APP_BASE_PATH)) {
|
|
13939
|
-
|
|
13950
|
+
// Live /admin: content comes from useAdminStudioContent — do not paint empty first.
|
|
13940
13951
|
setContentMode('cloud');
|
|
13941
|
-
|
|
13942
13952
|
setContentFallback(null);
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
setHasInitialCloudResolved(true);
|
|
13947
|
-
|
|
13953
|
+
setShowTopProgress(true);
|
|
13954
|
+
setHasInitialCloudResolved(false);
|
|
13948
13955
|
return;
|
|
13949
|
-
|
|
13950
13956
|
}
|
|
13951
13957
|
|
|
13952
13958
|
|
|
@@ -14243,7 +14249,7 @@ export function useTenantBootstrap({
|
|
|
14243
14249
|
|
|
14244
14250
|
isCloudMode,
|
|
14245
14251
|
|
|
14246
|
-
|
|
14252
|
+
bootSource,
|
|
14247
14253
|
|
|
14248
14254
|
useRenderBootstrap,
|
|
14249
14255
|
|
|
@@ -14289,9 +14295,7 @@ export function useTenantBootstrap({
|
|
|
14289
14295
|
|
|
14290
14296
|
isCloudMode,
|
|
14291
14297
|
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
isHotSaveMode,
|
|
14298
|
+
bootSource,
|
|
14295
14299
|
|
|
14296
14300
|
contentMode,
|
|
14297
14301
|
|
|
@@ -14305,6 +14309,8 @@ export function useTenantBootstrap({
|
|
|
14305
14309
|
|
|
14306
14310
|
isTenantEmpty,
|
|
14307
14311
|
|
|
14312
|
+
markCloudContentReady,
|
|
14313
|
+
|
|
14308
14314
|
retryBootstrap,
|
|
14309
14315
|
|
|
14310
14316
|
};
|