astro-tractstack 2.0.41 → 2.0.42

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.
Files changed (48) hide show
  1. package/dist/index.js +8 -2
  2. package/package.json +1 -1
  3. package/templates/src/components/Header.astro +1 -0
  4. package/templates/src/components/compositor/Node.tsx +4 -1
  5. package/templates/src/components/compositor/preview/PanesPreviewGenerator.tsx +5 -1
  6. package/templates/src/components/edit/SettingsPanel.tsx +1 -3
  7. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -10
  8. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +6 -2
  9. package/templates/src/components/edit/pane/PanePanel_path.tsx +4 -3
  10. package/templates/src/components/edit/panels/StyleParentPanel.tsx +0 -2
  11. package/templates/src/components/edit/state/SaveModal.tsx +250 -79
  12. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +27 -16
  13. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +5 -7
  14. package/templates/src/components/edit/widgets/BeliefWidget.tsx +4 -1
  15. package/templates/src/components/edit/widgets/IdentifyAsWidget.tsx +5 -1
  16. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +5 -1
  17. package/templates/src/components/edit/widgets/ToggleWidget.tsx +4 -1
  18. package/templates/src/components/fields/BackgroundImage.tsx +4 -1
  19. package/templates/src/components/fields/ImageUpload.tsx +4 -1
  20. package/templates/src/components/form/ActionBuilderField.tsx +5 -1
  21. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +4 -2
  22. package/templates/src/components/storykeep/state/BrandingWrapper.tsx +13 -1
  23. package/templates/src/components/storykeep/widgets/HydrateWizard.tsx +84 -0
  24. package/templates/src/components/storykeep/widgets/{SetupWizard.tsx → InitWizard.tsx} +4 -3
  25. package/templates/src/components/widgets/Impression.tsx +3 -1
  26. package/templates/src/hooks/useSearch.ts +5 -3
  27. package/templates/src/layouts/Layout.astro +1 -23
  28. package/templates/src/pages/[...slug]/edit.astro +0 -1
  29. package/templates/src/pages/api/auth/decode.ts +2 -4
  30. package/templates/src/pages/api/auth/login.ts +4 -5
  31. package/templates/src/pages/api/auth/logout.ts +22 -7
  32. package/templates/src/pages/api/auth/profile.ts +4 -2
  33. package/templates/src/pages/api/sandbox.ts +3 -5
  34. package/templates/src/pages/api/tailwind.ts +6 -9
  35. package/templates/src/pages/storykeep/branding.astro +18 -1
  36. package/templates/src/pages/storykeep/init.astro +2 -2
  37. package/templates/src/stores/analytics.ts +5 -14
  38. package/templates/src/stores/nodes.ts +1 -6
  39. package/templates/src/stores/orphanAnalysis.ts +5 -40
  40. package/templates/src/types/compositorTypes.ts +1 -1
  41. package/templates/src/types/tractstack.ts +2 -0
  42. package/templates/src/utils/actions/actionButton.ts +3 -1
  43. package/templates/src/utils/api/brandHelpers.ts +1 -0
  44. package/templates/src/utils/api/setupHelpers.ts +177 -20
  45. package/templates/src/utils/api.ts +14 -26
  46. package/templates/src/utils/compositor/nodesHelper.ts +5 -1
  47. package/templates/src/utils/tenantResolver.ts +1 -1
  48. package/utils/inject-files.ts +8 -2
@@ -6,6 +6,7 @@ import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
6
6
  import { getFullContentMap } from '@/stores/analytics';
7
7
  import { getBrandConfig } from '@/utils/api/brandConfig';
8
8
  import { preHealthCheck } from '@/utils/backend';
9
+ import type { LoadData } from '@/types/compositorTypes';
9
10
 
10
11
  const tenantId =
11
12
  Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
@@ -23,10 +24,25 @@ if (authCheck) {
23
24
  const userIsAuthenticated = isAuthenticated(Astro);
24
25
  const userIsAdmin = isAdmin(Astro);
25
26
  const role = userIsAdmin ? `admin` : userIsAuthenticated ? `editor` : null;
26
-
27
27
  const brandConfig = await getBrandConfig(tenantId);
28
28
  const initializing = !brandConfig.SITE_INIT;
29
29
 
30
+ let initialSuitcase: LoadData | null = null;
31
+ if (initializing) {
32
+ const goBackend =
33
+ import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
34
+ try {
35
+ const response = await fetch(`${goBackend}/api/v1/setup/suitcase`, {
36
+ headers: { 'X-Tenant-Id': tenantId },
37
+ });
38
+ if (response.ok) {
39
+ initialSuitcase = await response.json();
40
+ }
41
+ } catch (e) {
42
+ console.warn('[Branding] Failed to probe suitcase:', e);
43
+ }
44
+ }
45
+
30
46
  const title = 'Branding | StoryKeep';
31
47
 
32
48
  let fullContentMap;
@@ -52,6 +68,7 @@ try {
52
68
  role={role}
53
69
  initializing={initializing}
54
70
  initialBrandConfig={brandConfig}
71
+ initialSuitcase={initialSuitcase}
55
72
  />
56
73
  </div>
57
74
  </main>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import { freshInstallStore } from '@/stores/backend';
3
3
  import { preHealthCheck } from '@/utils/backend';
4
- import SetupWizard from '@/components/storykeep/widgets/SetupWizard';
4
+ import InitWizard from '@/components/storykeep/widgets/InitWizard';
5
5
 
6
6
  const isMultiTenant = import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
7
7
 
@@ -41,7 +41,7 @@ const mainStylesUrl = isDev
41
41
  </div>
42
42
  ) : (
43
43
  <div class="max-w-5xl p-3.5 md:p-8">
44
- <SetupWizard client:load />
44
+ <InitWizard client:load />
45
45
  </div>
46
46
  )
47
47
  }
@@ -11,7 +11,6 @@ export interface AppliedFilter {
11
11
  value: string;
12
12
  }
13
13
 
14
- // Internal tenant-keyed storage
15
14
  const tenantEpinetCustomFilters = atom<
16
15
  Record<
17
16
  string,
@@ -50,13 +49,13 @@ const tenantFullContentMaps = atom<
50
49
 
51
50
  // Helper to get current tenant ID
52
51
  function getCurrentTenantId(): string {
53
- if (typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) {
54
- return window.TRACTSTACK_CONFIG.tenantId;
55
- }
56
- return import.meta.env.PUBLIC_TENANTID || 'default';
52
+ const resolvedTenantId =
53
+ (typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) ||
54
+ import.meta.env.PUBLIC_TENANTID ||
55
+ 'default';
56
+ return resolvedTenantId;
57
57
  }
58
58
 
59
- // Default filter state
60
59
  const defaultEpinetFilters = {
61
60
  enabled: false,
62
61
  visitorType: 'all' as 'all' | 'anonymous' | 'known',
@@ -69,7 +68,6 @@ const defaultEpinetFilters = {
69
68
  appliedFilters: [],
70
69
  };
71
70
 
72
- // Create tenant-aware atoms that work with useStore
73
71
  const createEpinetFiltersStore = () => {
74
72
  const store = {
75
73
  get: () => {
@@ -95,8 +93,6 @@ const createEpinetFiltersStore = () => {
95
93
  callback(filters[tenantId] || defaultEpinetFilters);
96
94
  });
97
95
  },
98
-
99
- // Required nanostore properties for useStore
100
96
  lc: 0,
101
97
  listen: function (callback: any) {
102
98
  return this.subscribe(callback);
@@ -131,8 +127,6 @@ const createFullContentMapStore = () => {
131
127
  callback(maps[tenantId] || null);
132
128
  });
133
129
  },
134
-
135
- // Required nanostore properties for useStore
136
130
  lc: 0,
137
131
  listen: function (callback: any) {
138
132
  return this.subscribe(callback);
@@ -152,15 +146,12 @@ export const fullContentMapStore = createFullContentMapStore();
152
146
 
153
147
  export async function getFullContentMap(tenantId: string): Promise<any[]> {
154
148
  const api = new TractStackAPI(tenantId);
155
-
156
- // Check tenant-specific cache
157
149
  const cached = tenantFullContentMaps.get()[tenantId];
158
150
 
159
151
  try {
160
152
  const response = await api.getContentMapWithTimestamp(cached?.lastUpdated);
161
153
 
162
154
  if (response.success && response.data) {
163
- // Update tenant-specific cache
164
155
  const newData = {
165
156
  data: response.data.data,
166
157
  lastUpdated: response.data.lastUpdated,
@@ -609,7 +609,7 @@ export class NodesContext {
609
609
  return { left: originalNode, right: null };
610
610
  }
611
611
 
612
- // Handle split at the beginning of the string (THE FIX)
612
+ // Handle split at the beginning of the string
613
613
  if (offset === 0) {
614
614
  if (VERBOSE)
615
615
  console.log(
@@ -1992,8 +1992,6 @@ export class NodesContext {
1992
1992
 
1993
1993
  let autoCreatedMarkdownNode: MarkdownPaneFragmentNode | null = null;
1994
1994
 
1995
- //console.log(`--- [TRAP - TEMPLATE BEFORE] ---`, cloneDeep(node));
1996
- // 3. HANDLE EMPTY PANE BY AUTO-CREATING A MARKDOWN NODE
1997
1995
  if (targetNode.nodeType === 'Pane') {
1998
1996
  // Create a minimal markdown node to act as the container
1999
1997
  const newMarkdownNode: MarkdownPaneFragmentNode = {
@@ -2070,8 +2068,6 @@ export class NodesContext {
2070
2068
  );
2071
2069
  }
2072
2070
 
2073
- //console.log(`--- [TRAP - FLATTENED AFTER] ---`, cloneDeep(flattenedNodes));
2074
-
2075
2071
  // 5. PERFORM REMAINING STATE MUTATIONS
2076
2072
  if (originalPaneNode) {
2077
2073
  this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
@@ -2838,7 +2834,6 @@ export class NodesContext {
2838
2834
 
2839
2835
  getDirtyNodesClassData(): { dirtyPaneIds: string[]; classes: string[] } {
2840
2836
  const dirtyNodes = this.getDirtyNodes();
2841
-
2842
2837
  const dirtyPaneIds = dirtyNodes
2843
2838
  .filter((node) => node.nodeType === 'Pane')
2844
2839
  .map((node) => node.id);
@@ -24,10 +24,11 @@ const tenantOrphanAnalysis = atom<Record<string, OrphanAnalysisState>>({});
24
24
 
25
25
  // Helper to get current tenant ID
26
26
  function getCurrentTenantId(): string {
27
- if (typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) {
28
- return window.TRACTSTACK_CONFIG.tenantId;
29
- }
30
- return import.meta.env.PUBLIC_TENANTID || 'default';
27
+ const resolvedTenantId =
28
+ (typeof window !== 'undefined' && window.TRACTSTACK_CONFIG?.tenantId) ||
29
+ import.meta.env.PUBLIC_TENANTID ||
30
+ 'default';
31
+ return resolvedTenantId;
31
32
  }
32
33
 
33
34
  // Default state
@@ -38,7 +39,6 @@ const defaultOrphanState: OrphanAnalysisState = {
38
39
  lastFetched: null,
39
40
  };
40
41
 
41
- // Create tenant-aware store that works with useStore
42
42
  const createOrphanAnalysisStore = () => {
43
43
  const store = {
44
44
  get: () => {
@@ -52,8 +52,6 @@ const createOrphanAnalysisStore = () => {
52
52
  callback(analysis[tenantId] || defaultOrphanState);
53
53
  });
54
54
  },
55
-
56
- // Required nanostore properties for useStore
57
55
  lc: 0,
58
56
  listen: function (callback: any) {
59
57
  return this.subscribe(callback);
@@ -71,7 +69,6 @@ const createOrphanAnalysisStore = () => {
71
69
 
72
70
  export const orphanAnalysisStore = createOrphanAnalysisStore();
73
71
 
74
- // Helper to update state for specific tenant
75
72
  function updateTenantState(
76
73
  tenantId: string,
77
74
  updates: Partial<OrphanAnalysisState>
@@ -88,7 +85,6 @@ function updateTenantState(
88
85
  });
89
86
  }
90
87
 
91
- // Helper function to count orphans from the analysis data
92
88
  export function countOrphans(data: OrphanAnalysisData | null): number {
93
89
  if (!data) return 0;
94
90
 
@@ -374,34 +370,3 @@ function stopPolling(tenantId: string): void {
374
370
  // Clean up polling state
375
371
  pollingState.delete(tenantId);
376
372
  }
377
-
378
- export function clearOrphanAnalysis(): void {
379
- const tenantId = getCurrentTenantId();
380
- stopPolling(tenantId);
381
- fetchingStates.set(tenantId, false);
382
-
383
- updateTenantState(tenantId, defaultOrphanState);
384
- }
385
-
386
- // Enhanced utility function to get polling status for debugging
387
- export function getPollingStatus(tenantId?: string): Record<string, any> {
388
- const targetTenantId = tenantId || getCurrentTenantId();
389
- const state = pollingState.get(targetTenantId);
390
- const isActive = pollingIntervals.has(targetTenantId);
391
-
392
- if (!state && !isActive) {
393
- return { status: 'inactive', tenantId: targetTenantId };
394
- }
395
-
396
- return {
397
- status: isActive ? 'active' : 'stopped',
398
- tenantId: targetTenantId,
399
- attempts: state?.attempts || 0,
400
- maxAttempts: MAX_POLLING_ATTEMPTS,
401
- consecutiveErrors: state?.consecutiveErrors || 0,
402
- startTime: state?.startTime,
403
- lastAttemptTime: state?.lastAttemptTime,
404
- elapsed: state?.startTime ? Date.now() - state.startTime : 0,
405
- maxDuration: MAX_POLLING_DURATION,
406
- };
407
- }
@@ -239,7 +239,7 @@ export interface PaneNode extends BaseNode {
239
239
  export interface StoryFragmentNode extends BaseNode {
240
240
  title: string;
241
241
  slug: string;
242
- tractStackId?: string;
242
+ tractStackId: string;
243
243
  paneIds: string[];
244
244
  menuId?: string;
245
245
  tailwindBgColour?: string;
@@ -184,6 +184,7 @@ export interface BrandConfig {
184
184
  KNOWN_RESOURCES?: KnownResourcesConfig;
185
185
  DESIGN_LIBRARY?: DesignLibraryConfig;
186
186
  HAS_AAI?: boolean;
187
+ HAS_HYDRATION_TOKEN?: boolean;
187
188
  }
188
189
 
189
190
  export interface BrandConfigState {
@@ -217,6 +218,7 @@ export interface BrandConfigState {
217
218
  knownResources: KnownResourcesConfig;
218
219
  designLibrary?: DesignLibraryConfig;
219
220
  hasAAI: boolean;
221
+ hasHydrationToken: boolean;
220
222
  }
221
223
 
222
224
  // Form validation types
@@ -18,6 +18,8 @@ async function sendAnalyticsEvent(event: {
18
18
  try {
19
19
  const config = window.TRACTSTACK_CONFIG;
20
20
  if (!config || !config.sessionId) return;
21
+ const backendUrl =
22
+ import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
21
23
 
22
24
  const sessionId = config.sessionId;
23
25
  const formData: { [key: string]: string } = {
@@ -31,7 +33,7 @@ async function sendAnalyticsEvent(event: {
31
33
  formData.duration = event.duration.toString();
32
34
  }
33
35
 
34
- await fetch(`${config.backendUrl}/api/v1/state`, {
36
+ await fetch(`${backendUrl}/api/v1/state`, {
35
37
  method: 'POST',
36
38
  headers: {
37
39
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -40,6 +40,7 @@ export function convertToLocalState(
40
40
  knownResources: brandConfig.KNOWN_RESOURCES ?? {},
41
41
  designLibrary: brandConfig.DESIGN_LIBRARY ?? undefined,
42
42
  hasAAI: brandConfig.HAS_AAI ?? false,
43
+ hasHydrationToken: brandConfig.HAS_HYDRATION_TOKEN ?? false,
43
44
  };
44
45
  }
45
46
 
@@ -1,4 +1,10 @@
1
+ import { getCtx } from '@/stores/nodes';
1
2
  import { TractStackAPI } from '@/utils/api';
3
+ import { pendingHomePageSlugStore } from '@/stores/storykeep';
4
+ import type { FullContentMapItem } from '@/types/tractstack';
5
+ import type { LoadData, TractStackNode } from '@/types/compositorTypes';
6
+
7
+ const VERBOSE = false;
2
8
 
3
9
  export interface SetupWizardState {
4
10
  email: string;
@@ -18,43 +24,57 @@ export const initialSetupState: SetupWizardState = {
18
24
  tursoAuthToken: '',
19
25
  };
20
26
 
21
- /**
22
- * State interceptor (Preserving existing UI patterns)
23
- */
24
27
  export function setupStateIntercept(
25
28
  newState: SetupWizardState,
26
29
  field: keyof SetupWizardState,
27
30
  value: any
28
31
  ): SetupWizardState {
29
- // Pattern: Clear Turso fields when disabled
32
+ if (VERBOSE)
33
+ console.log(
34
+ `[setupStateIntercept] Intercepting field: ${String(field)}, Value:`,
35
+ value
36
+ );
37
+
30
38
  if (field === 'tursoEnabled' && !value) {
31
- return {
39
+ const result = {
32
40
  ...newState,
33
41
  tursoEnabled: false,
34
42
  tursoDatabaseURL: '',
35
43
  tursoAuthToken: '',
36
44
  };
45
+ if (VERBOSE)
46
+ console.log(
47
+ '[setupStateIntercept] Turso disabled. Resetting Turso fields.',
48
+ result
49
+ );
50
+ return result;
37
51
  }
38
52
 
39
- // Pattern: Clear confirmation password when main password changes
40
53
  if (field === 'adminPassword') {
41
- return {
54
+ const result = {
42
55
  ...newState,
43
56
  adminPassword: value,
44
57
  confirmPassword: '',
45
58
  };
59
+ if (VERBOSE)
60
+ console.log(
61
+ '[setupStateIntercept] Admin password changed. Clearing confirm password.'
62
+ );
63
+ return result;
46
64
  }
47
65
 
66
+ if (VERBOSE)
67
+ console.log(
68
+ '[setupStateIntercept] No special handling required. Returning new state.'
69
+ );
48
70
  return newState;
49
71
  }
50
72
 
51
- /**
52
- * Validation Logic (Preserving existing Regex and Rules)
53
- */
54
73
  export function validateSetup(state: SetupWizardState): Record<string, string> {
74
+ if (VERBOSE)
75
+ console.log('[validateSetup] Starting validation for state:', state);
55
76
  const errors: Record<string, string> = {};
56
77
 
57
- // Email Validation pattern
58
78
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
59
79
  if (!state.email.trim()) {
60
80
  errors.email = 'Email is required';
@@ -62,19 +82,16 @@ export function validateSetup(state: SetupWizardState): Record<string, string> {
62
82
  errors.email = 'Please enter a valid email address';
63
83
  }
64
84
 
65
- // Password Validation pattern
66
85
  if (!state.adminPassword.trim()) {
67
86
  errors.adminPassword = 'Admin password is required';
68
87
  } else if (state.adminPassword.length < 8) {
69
88
  errors.adminPassword = 'Admin password must be at least 8 characters long';
70
89
  }
71
90
 
72
- // Confirmation Pattern
73
91
  if (!errors.adminPassword && state.adminPassword !== state.confirmPassword) {
74
92
  errors.confirmPassword = 'Passwords do not match';
75
93
  }
76
94
 
77
- // Turso Validation Pattern
78
95
  if (state.tursoEnabled) {
79
96
  if (!state.tursoDatabaseURL.trim()) {
80
97
  errors.tursoDatabaseURL = 'Turso Database URL is required';
@@ -88,14 +105,26 @@ export function validateSetup(state: SetupWizardState): Record<string, string> {
88
105
  }
89
106
  }
90
107
 
108
+ if (Object.keys(errors).length > 0) {
109
+ if (VERBOSE)
110
+ console.error('[validateSetup] Validation failed with errors:', errors);
111
+ } else {
112
+ if (VERBOSE) console.log('[validateSetup] Validation successful.');
113
+ }
114
+
91
115
  return errors;
92
116
  }
93
117
 
94
- /**
95
- * API Call (Preserving Payload Structure)
96
- */
97
118
  export async function initializeSystem(state: SetupWizardState): Promise<void> {
98
- const api = new TractStackAPI('default');
119
+ if (VERBOSE)
120
+ console.log('[initializeSystem] Starting system initialization.');
121
+
122
+ const tenantId =
123
+ window.TRACTSTACK_CONFIG?.tenantId ||
124
+ import.meta.env.PUBLIC_TENANTID ||
125
+ 'default';
126
+
127
+ const api = new TractStackAPI(tenantId);
99
128
 
100
129
  const payload = {
101
130
  adminEmail: state.email.trim(),
@@ -106,9 +135,137 @@ export async function initializeSystem(state: SetupWizardState): Promise<void> {
106
135
  }),
107
136
  };
108
137
 
109
- const response = await api.post('/api/v1/setup/initialize', payload);
138
+ const endpoint = '/api/v1/setup/initialize';
139
+ if (VERBOSE)
140
+ console.log(
141
+ `[initializeSystem] POSTing to ${endpoint}. Payload (passwords masked):`,
142
+ {
143
+ ...payload,
144
+ adminPassword: '***',
145
+ }
146
+ );
147
+
148
+ const response = await api.post(endpoint, payload);
149
+ if (VERBOSE)
150
+ console.log('[initializeSystem] API Response received:', response);
110
151
 
111
152
  if (!response.success) {
112
- throw new Error(response.error || 'Setup failed');
153
+ const errorMessage = response.error || 'Setup failed';
154
+ if (VERBOSE)
155
+ console.error('[initializeSystem] Setup failed. Throwing error.');
156
+ throw new Error(errorMessage);
157
+ }
158
+
159
+ if (VERBOSE)
160
+ console.log('[initializeSystem] System initialization successful.');
161
+ }
162
+
163
+ function forceMarkAllDirty(ctx: any) {
164
+ if (VERBOSE)
165
+ console.log('[forceMarkAllDirty] Flagging all nodes for SaveModal...');
166
+ const allNodes = Array.from(ctx.allNodes.get().values());
167
+ const dirtyUpdates = allNodes.map((n: any) => ({ ...n, isChanged: true }));
168
+ ctx.modifyNodes(dirtyUpdates);
169
+ }
170
+
171
+ export function prepareHydrationContext(
172
+ data: LoadData,
173
+ fullContentMap?: FullContentMapItem[]
174
+ ): void {
175
+ if (VERBOSE)
176
+ console.log(
177
+ '[prepareHydrationContext] Preparing context for suitcase hydration.'
178
+ );
179
+
180
+ const ctx = getCtx();
181
+
182
+ if (!fullContentMap) {
183
+ throw new Error(
184
+ 'Content map is required for suitcase installation to perform strict takeover.'
185
+ );
186
+ }
187
+
188
+ const tractStackNodeItem = fullContentMap.find(
189
+ (n) => n.type === 'TractStack'
190
+ );
191
+ if (!tractStackNodeItem) {
192
+ throw new Error(
193
+ 'Missing TractStack node in content map. Cannot link content.'
194
+ );
113
195
  }
196
+
197
+ const helloFragment = fullContentMap.find(
198
+ (n) =>
199
+ (n.type === 'StoryFragment' || n.type === 'StoryFragment') &&
200
+ n.slug === 'hello'
201
+ );
202
+ if (!helloFragment) {
203
+ throw new Error(
204
+ "Missing 'hello' StoryFragment in content map. Cannot perform takeover."
205
+ );
206
+ }
207
+
208
+ if (!data.storyfragmentNodes || data.storyfragmentNodes.length !== 1) {
209
+ throw new Error(
210
+ 'Suitcase must contain exactly one StoryFragment for takeover hydration.'
211
+ );
212
+ }
213
+
214
+ const incomingFragment = data.storyfragmentNodes[0];
215
+ const oldId = incomingFragment.id;
216
+ const targetId = helloFragment.id;
217
+
218
+ if (VERBOSE)
219
+ console.log(
220
+ `[prepareHydrationContext] TAKEOVER: Mapping 'hello' (${targetId}) with content from '${incomingFragment.title}'`
221
+ );
222
+
223
+ incomingFragment.id = targetId;
224
+ incomingFragment.slug = 'hello';
225
+ incomingFragment.tractStackId = tractStackNodeItem.id;
226
+ incomingFragment.parentId = tractStackNodeItem.id;
227
+
228
+ if (!data.tractstackNodes) {
229
+ data.tractstackNodes = [];
230
+ }
231
+ const rootTractStack = {
232
+ id: tractStackNodeItem.id,
233
+ nodeType: 'TractStack' as const,
234
+ parentId: null,
235
+ title: tractStackNodeItem.title || 'Tract Stack',
236
+ slug: tractStackNodeItem.slug || 'root',
237
+ socialImagePath: '',
238
+ };
239
+ data.tractstackNodes.push(rootTractStack as TractStackNode);
240
+
241
+ if (data.paneNodes) {
242
+ let patchedPanes = 0;
243
+ data.paneNodes.forEach((pane) => {
244
+ if (pane.parentId === oldId) {
245
+ pane.parentId = targetId;
246
+ patchedPanes++;
247
+ }
248
+ });
249
+ if (VERBOSE)
250
+ console.log(
251
+ `[prepareHydrationContext] Re-parented ${patchedPanes} panes from ${oldId} to ${targetId}`
252
+ );
253
+ }
254
+
255
+ if (VERBOSE)
256
+ console.log(`[prepareHydrationContext] Building the Nodes Context tree`);
257
+
258
+ ctx.buildNodesTreeFromRowDataMadeNodes(data);
259
+
260
+ if (VERBOSE)
261
+ console.log(
262
+ `[prepareHydrationContext] Marking the Nodes Context tree as dirty`
263
+ );
264
+ forceMarkAllDirty(ctx);
265
+
266
+ if (VERBOSE)
267
+ console.log(
268
+ '[prepareHydrationContext] Setting pending home page slug to "hello"'
269
+ );
270
+ pendingHomePageSlugStore.set('hello');
114
271
  }
@@ -20,28 +20,13 @@ export interface TractStackEvent {
20
20
  }
21
21
 
22
22
  function getConfig() {
23
- if (typeof window === 'undefined') {
24
- return {
25
- goBackend: import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080',
26
- tenantId: import.meta.env.PUBLIC_TENANTID || 'default',
27
- };
28
- }
29
-
30
23
  return {
31
- goBackend:
32
- window.TRACTSTACK_CONFIG?.backendUrl ||
33
- import.meta.env.PUBLIC_GO_BACKEND ||
34
- 'http://localhost:8080',
35
- tenantId:
36
- window.TRACTSTACK_CONFIG?.tenantId ||
37
- import.meta.env.PUBLIC_TENANTID ||
38
- 'default',
24
+ goBackend: import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080',
39
25
  };
40
26
  }
41
27
 
42
28
  export class TractStackAPI {
43
29
  private explicitTenantId?: string;
44
-
45
30
  constructor(tenantId?: string) {
46
31
  this.explicitTenantId = tenantId;
47
32
  }
@@ -51,7 +36,19 @@ export class TractStackAPI {
51
36
  options: RequestInit = {}
52
37
  ): Promise<APIResponse<T>> {
53
38
  const config = getConfig();
54
- const effectiveTenantId = this.explicitTenantId || config.tenantId;
39
+
40
+ const effectiveTenantId = this.explicitTenantId;
41
+
42
+ if (!effectiveTenantId) {
43
+ console.error(
44
+ '[TractStackAPI] CRITICAL ERROR: Tenant ID is required but was not provided to the constructor. Failing request.'
45
+ );
46
+ return {
47
+ success: false,
48
+ error: 'Tenant ID missing. Must be provided in constructor.',
49
+ };
50
+ }
51
+
55
52
  const baseUrl = config.goBackend;
56
53
 
57
54
  const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
@@ -147,15 +144,6 @@ export class TractStackAPI {
147
144
  return this.get(`/api/v1/fragments/${fragmentId}`);
148
145
  }
149
146
 
150
- getTenantId(): string {
151
- const config = getConfig();
152
- return this.explicitTenantId || config.tenantId;
153
- }
154
-
155
- setTenantId(tenantId: string): void {
156
- this.explicitTenantId = tenantId;
157
- }
158
-
159
147
  async getContentMapWithTimestamp(
160
148
  lastUpdated?: number
161
149
  ): Promise<APIResponse<{ data: any[]; lastUpdated: number }>> {
@@ -326,6 +326,7 @@ export function createEmptyStorykeep(id: string) {
326
326
  return {
327
327
  id,
328
328
  nodeType: 'StoryFragment',
329
+ tractStackId: 'temp',
329
330
  parentId: null,
330
331
  isChanged: false,
331
332
  paneIds: [],
@@ -503,13 +504,16 @@ export function extractClassesFromNodes(dirtyNodes: BaseNode[]): string[] {
503
504
  const uniqueClasses = new Set<string>();
504
505
 
505
506
  dirtyNodes.forEach((node) => {
506
- // Extract from parentCss arrays (like legacy getTailwindWhitelist)
507
507
  if ('parentCss' in node && Array.isArray(node.parentCss)) {
508
508
  node.parentCss.forEach((classString: string) => {
509
509
  classString.split(' ').forEach((className: string) => {
510
510
  if (className.trim()) uniqueClasses.add(className.trim());
511
511
  });
512
512
  });
513
+ } else if ('parentCss' in node && typeof node.parentCss === `string`) {
514
+ node.parentCss.split(' ').forEach((className: string) => {
515
+ if (className.trim()) uniqueClasses.add(className.trim());
516
+ });
513
517
  }
514
518
 
515
519
  // Extract from elementCss strings (like legacy getTailwindWhitelist)
@@ -1,4 +1,4 @@
1
- const VERBOSE = true;
1
+ const VERBOSE = false;
2
2
 
3
3
  interface TenantResolution {
4
4
  id: string;
@@ -2105,9 +2105,15 @@ export async function injectTemplateFiles(
2105
2105
  },
2106
2106
  {
2107
2107
  src: resolve(
2108
- '../templates/src/components/storykeep/widgets/SetupWizard.tsx'
2108
+ '../templates/src/components/storykeep/widgets/HydrateWizard.tsx'
2109
2109
  ),
2110
- dest: 'src/components/storykeep/widgets/SetupWizard.tsx',
2110
+ dest: 'src/components/storykeep/widgets/HydrateWizard.tsx',
2111
+ },
2112
+ {
2113
+ src: resolve(
2114
+ '../templates/src/components/storykeep/widgets/InitWizard.tsx'
2115
+ ),
2116
+ dest: 'src/components/storykeep/widgets/InitWizard.tsx',
2111
2117
  },
2112
2118
  {
2113
2119
  src: resolve('../templates/src/pages/storykeep/init.astro'),