@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.
@@ -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.17",
1821
- "@olonjs/react": "^0.1.0",
1822
- "@olonjs/studio": "^0.1.0",
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, CLOUD_API_KEY, CLOUD_API_URL, TENANT_ID } from '@/lib/tenantEnv';
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.isHotSaveMode,
3013
+ enabled: bootstrap.bootSource === 'live',
3014
3014
  apiCandidates: bootstrap.cloudApiCandidates,
3015
- apiKey: CLOUD_API_KEY,
3015
+ apiKey: cloudPolicy.apiKey,
3016
3016
  setPages: bootstrap.setPages,
3017
3017
  setSiteConfig: bootstrap.setSiteConfig,
3018
- setCollections: bootstrap.setCollections,
3018
+ onSettled: bootstrap.markCloudContentReady,
3019
3019
  });
3020
- const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(bootstrap.isCloudMode);
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.isHotSaveMode || Object.keys(bootstrap.collections).length > 0
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
- async hotSave(state: ProjectState, slug: string): Promise<void> {
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: !bootstrap.isCloudMode,
3151
- showHotSave: bootstrap.isHotSaveMode,
3152
- showColdSave: bootstrap.isSave2RepoMode,
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: bootstrap.isCloudMode,
3161
- cloudApiUrl: CLOUD_API_URL,
3162
- cloudApiKey: CLOUD_API_KEY,
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={bootstrap.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 missingEnv =
4097
- !import.meta.env.VITE_JSONPAGES_CLOUD_URL &&
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: !missingEnv,
4109
- label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
4110
- code: 'VITE_JSONPAGES_CLOUD_URL=https://cloud.olonjs.io',
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: !!import.meta.env.VITE_JSONPAGES_API_KEY || !!import.meta.env.VITE_OLONJS_API_KEY,
4114
- label: 'VITE_JSONPAGES_API_KEY nel file .env',
4115
- code: 'VITE_JSONPAGES_API_KEY=sk-...',
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
- setCollections: Dispatch<SetStateAction<NonNullable<JsonPagesConfig['collections']>>>;
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
- setCollections,
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
- if (!enabled || apiCandidates.length === 0 || !apiKey.trim()) return;
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, setCollections]);
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
- function normalizeApiBase(raw: string): string {
11895
- return raw.trim().replace(/\/+$/, '');
11896
- }
11907
+ import { buildApiCandidates } from '@olonjs/react';
11897
11908
 
11898
- export function buildApiCandidates(raw: string): string[] {
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
- const apiUrl =
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
- export const CLOUD_API_URL =
13095
- import.meta.env.VITE_OLONJS_CLOUD_URL ?? import.meta.env.VITE_JSONPAGES_CLOUD_URL;
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 '@/lib/sppCloudConfig';
13108
- import { CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
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
- export function useAssetsManifest(isCloudMode: boolean) {
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 && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
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 && CLOUD_API_URL && CLOUD_API_KEY) {
13123
- const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
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 ${CLOUD_API_KEY}` },
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 cloudApiUrl = import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined;
13420
- const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
13434
+ const { apiUrl, apiKey } = cloudPolicy;
13421
13435
 
13422
- if (!cloudApiUrl || !cloudApiKey) {
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
- // Trasformiamo FormData in un oggetto piatto per il payload JSON
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 = cloudApiUrl.replace(/\/$/, '');
13463
+ const apiBase = apiUrl.replace(/\/$/, '');
13452
13464
  const response = await fetch(`${apiBase}/forms/submit`, {
13453
13465
  method: 'POST',
13454
13466
  headers: {
13455
- Authorization: `Bearer ${cloudApiKey}`,
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
- const API_BASE =
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 VITE_OLONJS_CLOUD_URL/forms/submit */
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
- : API_BASE
13523
- ? API_BASE.replace(/\/$/, '')
13532
+ : apiUrl
13533
+ ? apiUrl.replace(/\/$/, '')
13524
13534
  : null;
13525
13535
 
13526
- if (!resolvedBase || !API_KEY) {
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 ${API_KEY}`,
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, SAVE2REPO_ENABLED } from '@/lib/tenantEnv';
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 = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
13733
-
13734
- const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
13742
+ const { isCloudMode, bootSource } = cloudPolicy;
13735
13743
 
13736
- const isHotSaveMode = isCloudMode && !isSave2RepoMode;
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 (isSave2RepoMode) {
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
- setShowTopProgress(false);
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
- isSave2RepoMode,
14252
+ bootSource,
14247
14253
 
14248
14254
  useRenderBootstrap,
14249
14255
 
@@ -14289,9 +14295,7 @@ export function useTenantBootstrap({
14289
14295
 
14290
14296
  isCloudMode,
14291
14297
 
14292
- isSave2RepoMode,
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.17",
1821
- "@olonjs/react": "^0.1.0",
1822
- "@olonjs/studio": "^0.1.0",
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, CLOUD_API_KEY, CLOUD_API_URL, TENANT_ID } from '@/lib/tenantEnv';
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.isHotSaveMode,
3013
+ enabled: bootstrap.bootSource === 'live',
3014
3014
  apiCandidates: bootstrap.cloudApiCandidates,
3015
- apiKey: CLOUD_API_KEY,
3015
+ apiKey: cloudPolicy.apiKey,
3016
3016
  setPages: bootstrap.setPages,
3017
3017
  setSiteConfig: bootstrap.setSiteConfig,
3018
- setCollections: bootstrap.setCollections,
3018
+ onSettled: bootstrap.markCloudContentReady,
3019
3019
  });
3020
- const { assetsManifest, loadAssetsManifest, cloudApiCandidates } = useAssetsManifest(bootstrap.isCloudMode);
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.isHotSaveMode || Object.keys(bootstrap.collections).length > 0
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
- async hotSave(state: ProjectState, slug: string): Promise<void> {
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: !bootstrap.isCloudMode,
3151
- showHotSave: bootstrap.isHotSaveMode,
3152
- showColdSave: bootstrap.isSave2RepoMode,
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: bootstrap.isCloudMode,
3161
- cloudApiUrl: CLOUD_API_URL,
3162
- cloudApiKey: CLOUD_API_KEY,
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={bootstrap.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 missingEnv =
4097
- !import.meta.env.VITE_JSONPAGES_CLOUD_URL &&
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: !missingEnv,
4109
- label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
4110
- code: 'VITE_JSONPAGES_CLOUD_URL=https://cloud.olonjs.io',
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: !!import.meta.env.VITE_JSONPAGES_API_KEY || !!import.meta.env.VITE_OLONJS_API_KEY,
4114
- label: 'VITE_JSONPAGES_API_KEY nel file .env',
4115
- code: 'VITE_JSONPAGES_API_KEY=sk-...',
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
- setCollections: Dispatch<SetStateAction<NonNullable<JsonPagesConfig['collections']>>>;
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
- setCollections,
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
- if (!enabled || apiCandidates.length === 0 || !apiKey.trim()) return;
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, setCollections]);
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
- function normalizeApiBase(raw: string): string {
11895
- return raw.trim().replace(/\/+$/, '');
11896
- }
11907
+ import { buildApiCandidates } from '@olonjs/react';
11897
11908
 
11898
- export function buildApiCandidates(raw: string): string[] {
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
- const apiUrl =
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
- export const CLOUD_API_URL =
13095
- import.meta.env.VITE_OLONJS_CLOUD_URL ?? import.meta.env.VITE_JSONPAGES_CLOUD_URL;
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 '@/lib/sppCloudConfig';
13108
- import { CLOUD_API_KEY, CLOUD_API_URL } from '@/lib/tenantEnv';
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
- export function useAssetsManifest(isCloudMode: boolean) {
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 && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
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 && CLOUD_API_URL && CLOUD_API_KEY) {
13123
- const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
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 ${CLOUD_API_KEY}` },
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 cloudApiUrl = import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined;
13420
- const cloudApiKey = import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined;
13434
+ const { apiUrl, apiKey } = cloudPolicy;
13421
13435
 
13422
- if (!cloudApiUrl || !cloudApiKey) {
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
- // Trasformiamo FormData in un oggetto piatto per il payload JSON
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 = cloudApiUrl.replace(/\/$/, '');
13463
+ const apiBase = apiUrl.replace(/\/$/, '');
13452
13464
  const response = await fetch(`${apiBase}/forms/submit`, {
13453
13465
  method: 'POST',
13454
13466
  headers: {
13455
- Authorization: `Bearer ${cloudApiKey}`,
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
- const API_BASE =
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 VITE_OLONJS_CLOUD_URL/forms/submit */
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
- : API_BASE
13523
- ? API_BASE.replace(/\/$/, '')
13532
+ : apiUrl
13533
+ ? apiUrl.replace(/\/$/, '')
13524
13534
  : null;
13525
13535
 
13526
- if (!resolvedBase || !API_KEY) {
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 ${API_KEY}`,
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, SAVE2REPO_ENABLED } from '@/lib/tenantEnv';
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 = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
13733
-
13734
- const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
13742
+ const { isCloudMode, bootSource } = cloudPolicy;
13735
13743
 
13736
- const isHotSaveMode = isCloudMode && !isSave2RepoMode;
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 (isSave2RepoMode) {
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
- setShowTopProgress(false);
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
- isSave2RepoMode,
14252
+ bootSource,
14247
14253
 
14248
14254
  useRenderBootstrap,
14249
14255
 
@@ -14289,9 +14295,7 @@ export function useTenantBootstrap({
14289
14295
 
14290
14296
  isCloudMode,
14291
14297
 
14292
- isSave2RepoMode,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olonjs/cli",
3
- "version": "3.0.150",
3
+ "version": "3.0.152",
4
4
  "description": "The Sovereign CLI Engine for OlonJS.",
5
5
  "type": "module",
6
6
  "bin": {