astro-tractstack 2.0.40 → 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 +13 -13
  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
package/dist/index.js CHANGED
@@ -2068,9 +2068,15 @@ async function w(t, e, c) {
2068
2068
  },
2069
2069
  {
2070
2070
  src: t(
2071
- "../templates/src/components/storykeep/widgets/SetupWizard.tsx"
2071
+ "../templates/src/components/storykeep/widgets/HydrateWizard.tsx"
2072
2072
  ),
2073
- dest: "src/components/storykeep/widgets/SetupWizard.tsx"
2073
+ dest: "src/components/storykeep/widgets/HydrateWizard.tsx"
2074
+ },
2075
+ {
2076
+ src: t(
2077
+ "../templates/src/components/storykeep/widgets/InitWizard.tsx"
2078
+ ),
2079
+ dest: "src/components/storykeep/widgets/InitWizard.tsx"
2074
2080
  },
2075
2081
  {
2076
2082
  src: t("../templates/src/pages/storykeep/init.astro"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.40",
3
+ "version": "2.0.42",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,6 +34,7 @@ const {
34
34
  } = Astro.props;
35
35
 
36
36
  const isHome = slug === brandConfig?.HOME_SLUG;
37
+ console.log(slug, isHome);
37
38
 
38
39
  const tenantId =
39
40
  Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
@@ -198,7 +198,10 @@ const getElement = (
198
198
  panelType="storyfragment"
199
199
  ctx={getCtx(props)}
200
200
  >
201
- <StoryFragmentConfigPanel nodeId={props.nodeId} />
201
+ <StoryFragmentConfigPanel
202
+ nodeId={props.nodeId}
203
+ isSandboxMode={props.isSandboxMode || false}
204
+ />
202
205
  </PanelVisibilityWrapper>
203
206
  <StoryFragment {...sharedProps} />
204
207
  </>
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { transformLivePaneForPreview } from '@/utils/etl';
3
3
  import type { NodesContext } from '@/stores/nodes';
4
+ import { TractStackAPI } from '@/utils/api';
4
5
 
5
6
  export interface PanePreviewRequest {
6
7
  id: string;
@@ -25,6 +26,10 @@ export const PanesPreviewGenerator = ({
25
26
  onError,
26
27
  }: PanesPreviewGeneratorProps) => {
27
28
  const [isGenerating, setIsGenerating] = useState(false);
29
+ const tenantId =
30
+ window.TRACTSTACK_CONFIG?.tenantId ||
31
+ import.meta.env.PUBLIC_TENANTID ||
32
+ 'default';
28
33
 
29
34
  useEffect(() => {
30
35
  if (requests.length === 0) return;
@@ -67,27 +72,22 @@ export const PanesPreviewGenerator = ({
67
72
  requestMap.set(previewPayload.id, request.id);
68
73
  }
69
74
 
70
- const goBackend =
71
- import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
72
- const response = await fetch(`${goBackend}/api/v1/fragments/preview`, {
73
- method: 'POST',
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- 'X-Tenant-ID': import.meta.env.PUBLIC_TENANTID || 'default',
77
- },
78
- body: JSON.stringify({ panes: previewPayloads }),
75
+ const api = new TractStackAPI(tenantId);
76
+ const response = await api.post('/api/v1/fragments/preview', {
77
+ panes: previewPayloads,
79
78
  });
80
79
 
81
- if (!response.ok) {
82
- throw new Error(`Preview API failed: ${response.status}`);
80
+ if (!response.success) {
81
+ throw new Error(response.error || `Preview API failed`);
83
82
  }
84
83
 
85
- const { fragments, errors } = await response.json();
84
+ // TractStackAPI unwraps the response.data for us
85
+ const { fragments, errors } = response.data;
86
86
 
87
87
  const results: PaneFragmentResult[] = [];
88
88
 
89
89
  for (const [paneId, requestId] of requestMap.entries()) {
90
- if (fragments[paneId]) {
90
+ if (fragments && fragments[paneId]) {
91
91
  results.push({
92
92
  id: requestId,
93
93
  htmlString: fragments[paneId],
@@ -4,14 +4,12 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
4
4
  import { settingsPanelStore } from '@/stores/storykeep';
5
5
  import { getCtx } from '@/stores/nodes';
6
6
  import PanelSwitch from './PanelSwitch';
7
- import type { BrandConfig } from '@/types/tractstack';
8
7
 
9
8
  interface SettingsPanelProps {
10
- config: BrandConfig;
11
9
  availableCodeHooks: string[];
12
10
  }
13
11
 
14
- const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
12
+ const SettingsPanel = ({ availableCodeHooks }: SettingsPanelProps) => {
15
13
  const [panelTitle, setPanelTitle] = useState('Settings');
16
14
  const signal = useStore(settingsPanelStore);
17
15
  const ctx = getCtx();
@@ -24,7 +24,7 @@ import { DirectInjectStep } from './steps/DirectInjectStep';
24
24
  import BooleanToggle from '@/components/form/BooleanToggle';
25
25
  import EnumSelect from '@/components/form/EnumSelect';
26
26
  import type { StoryFragmentNode } from '@/types/compositorTypes';
27
- import { TractStackAPI } from '@/utils/api'; // <--- IMPORT ADDED
27
+ import { TractStackAPI } from '@/utils/api';
28
28
 
29
29
  type Step =
30
30
  | 'initial'
@@ -40,21 +40,17 @@ type InitialChoice = 'library' | 'ai' | 'blank';
40
40
  type LayoutChoice = 'standard' | 'grid';
41
41
  type ColumnPresetKey = 'left' | 'right';
42
42
 
43
- interface GenerationResponse {
44
- success: boolean;
45
- data?: { response: string | object };
46
- error?: string;
47
- }
48
-
49
43
  const callAskLemurAPI = async (
50
44
  prompt: string,
51
45
  context: string,
52
46
  expectJson: boolean,
53
47
  isSandboxMode: boolean
54
48
  ): Promise<string> => {
55
- // FIX: Use the centralized API class to ensure correct Tenant ID resolution
56
- const api = new TractStackAPI();
57
- const tenantId = api.getTenantId(); // Gets correct ID from window config
49
+ const tenantId =
50
+ window.TRACTSTACK_CONFIG?.tenantId ||
51
+ import.meta.env.PUBLIC_TENANTID ||
52
+ 'default';
53
+ const api = new TractStackAPI(tenantId);
58
54
 
59
55
  const requestBody = {
60
56
  prompt,
@@ -34,6 +34,10 @@ const AddPaneReUsePanel = ({
34
34
  first,
35
35
  setMode,
36
36
  }: AddPaneReUsePanelProps) => {
37
+ const tenantId =
38
+ window.TRACTSTACK_CONFIG?.tenantId ||
39
+ import.meta.env.PUBLIC_TENANTID ||
40
+ 'default';
37
41
  const [previews, setPreviews] = useState<PreviewItem[]>([]);
38
42
  const [query, setQuery] = useState('');
39
43
  const [availablePanes, setAvailablePanes] = useState<FullContentMapItem[]>(
@@ -128,7 +132,7 @@ const AddPaneReUsePanel = ({
128
132
  const fetchFragments = async () => {
129
133
  try {
130
134
  const paneIds = visiblePreviews.map((preview) => preview.pane.id);
131
- const api = new TractStackAPI();
135
+ const api = new TractStackAPI(tenantId);
132
136
 
133
137
  const response = await api.post('/api/v1/fragments/panes', { paneIds });
134
138
 
@@ -199,7 +203,7 @@ const AddPaneReUsePanel = ({
199
203
  if (!selectedPaneId) return;
200
204
 
201
205
  try {
202
- const api = new TractStackAPI();
206
+ const api = new TractStackAPI(tenantId);
203
207
  const response = await api.get(
204
208
  `/api/v1/nodes/panes/${selectedPaneId}/template`
205
209
  );
@@ -27,7 +27,10 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
27
27
  const [availableBeliefs, setAvailableBeliefs] = useState<BeliefNode[]>([]);
28
28
  const [isLoading, setIsLoading] = useState(true);
29
29
  const [error, setError] = useState<string | null>(null);
30
- //const [isCreatingBelief, setIsCreatingBelief] = useState(false);
30
+ const tenantId =
31
+ window.TRACTSTACK_CONFIG?.tenantId ||
32
+ import.meta.env.PUBLIC_TENANTID ||
33
+ 'default';
31
34
 
32
35
  const ctx = getCtx();
33
36
  const allNodes = ctx.allNodes.get();
@@ -46,7 +49,6 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
46
49
  setIsLoading(true);
47
50
  const goBackend =
48
51
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
49
- const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
50
52
 
51
53
  // Get all belief IDs first
52
54
  const idsResponse = await fetch(`${goBackend}/api/v1/nodes/beliefs`, {
@@ -156,7 +158,6 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
156
158
 
157
159
  const goBackend =
158
160
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
159
- const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
160
161
 
161
162
  const response = await fetch(
162
163
  `${goBackend}/api/v1/nodes/beliefs/${beliefId}`,
@@ -73,7 +73,6 @@ const StyleParentPanel = ({
73
73
  return;
74
74
  }
75
75
 
76
- // --- STABILIZATION FIX START ---
77
76
  let effectiveNode = initialNode;
78
77
  if (isMarkdownPaneFragmentNode(initialNode) && initialNode.parentId) {
79
78
  const parent = ctx.allNodes.get().get(initialNode.parentId);
@@ -81,7 +80,6 @@ const StyleParentPanel = ({
81
80
  effectiveNode = parent as GridLayoutNode;
82
81
  }
83
82
  }
84
- // --- STABILIZATION FIX END ---
85
83
 
86
84
  const targets: StyleableTarget[] = [];
87
85
  const isGrid = isGridLayoutNode(effectiveNode);
@@ -13,22 +13,25 @@ import {
13
13
  pendingHomePageSlugStore,
14
14
  } from '@/stores/storykeep';
15
15
  import { startLoadingAnimation } from '@/utils/helpers';
16
+ import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
16
17
  import type {
17
18
  FlatNode,
18
19
  BaseNode,
19
20
  PaneNode,
20
21
  StoryFragmentNode,
21
22
  MarkdownPaneFragmentNode,
23
+ GridLayoutNode,
22
24
  } from '@/types/compositorTypes';
23
25
 
24
26
  type SaveStage =
25
27
  | 'PREPARING'
26
28
  | 'SAVING_PENDING_FILES'
27
29
  | 'PROCESSING_OG_IMAGES'
30
+ | 'COOKING_NODES'
31
+ | 'PROCESSING_STYLES'
28
32
  | 'SAVING_PANES'
29
33
  | 'SAVING_STORY_FRAGMENTS'
30
34
  | 'LINKING_FILES'
31
- | 'PROCESSING_STYLES'
32
35
  | 'UPDATING_HOME_PAGE'
33
36
  | 'COMPLETED'
34
37
  | 'ERROR';
@@ -46,6 +49,7 @@ interface SaveModalProps {
46
49
  isContext: boolean;
47
50
  onClose: () => void;
48
51
  isSandboxMode?: boolean;
52
+ hydrate?: boolean;
49
53
  }
50
54
 
51
55
  const PROGRESS_PHASES = {
@@ -56,6 +60,7 @@ const PROGRESS_PHASES = {
56
60
  };
57
61
 
58
62
  const INDETERMINATE_STAGES: SaveStage[] = [
63
+ 'COOKING_NODES',
59
64
  'SAVING_PANES',
60
65
  'LINKING_FILES',
61
66
  'PROCESSING_STYLES',
@@ -109,6 +114,7 @@ export default function SaveModal({
109
114
  isContext,
110
115
  onClose,
111
116
  isSandboxMode = false,
117
+ hydrate = false,
112
118
  }: SaveModalProps) {
113
119
  const [stage, setStage] = useState<SaveStage>('PREPARING');
114
120
  const [progress, setProgress] = useState(0);
@@ -127,7 +133,10 @@ export default function SaveModal({
127
133
  const pendingHomePageSlug = pendingHomePageSlugStore.get();
128
134
  const goBackend =
129
135
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
130
- const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
136
+ const tenantId =
137
+ window.TRACTSTACK_CONFIG?.tenantId ||
138
+ import.meta.env.PUBLIC_TENANTID ||
139
+ 'default';
131
140
 
132
141
  const addDebugMessage = (message: string) => {
133
142
  const timestamp = new Date().toLocaleTimeString();
@@ -388,6 +397,203 @@ export default function SaveModal({
388
397
  dirtyPanes.length + dirtyStoryFragments.length;
389
398
  let completedProcessingSteps = 0;
390
399
 
400
+ // --- NEW COOKING STAGE ---
401
+ if (allDirtyNodes.length > 0) {
402
+ setStage('COOKING_NODES');
403
+ setIsIndeterminateStage(true);
404
+ addDebugMessage('Cooking nodes for whitelist extraction...');
405
+
406
+ const cookingUpdates: BaseNode[] = [];
407
+
408
+ allDirtyNodes.forEach((liveNode) => {
409
+ try {
410
+ let updatedNode: BaseNode | null = null;
411
+
412
+ // Pattern 1: TagElements -> elementCss
413
+ if (liveNode.nodeType === 'TagElement') {
414
+ const flatNode = liveNode as FlatNode;
415
+ const computedCSS = ctx.getNodeClasses(flatNode.id, 'auto', 0);
416
+ if (flatNode.elementCss !== computedCSS) {
417
+ updatedNode = {
418
+ ...liveNode,
419
+ elementCss: computedCSS,
420
+ } as FlatNode;
421
+ }
422
+ }
423
+ // Pattern 2: Markdown Nodes -> parentCss & gridCss
424
+ else if (liveNode.nodeType === 'Markdown') {
425
+ const markdownNode = liveNode as MarkdownPaneFragmentNode;
426
+ let needsUpdate = false;
427
+ const nextNode = { ...markdownNode };
428
+
429
+ // parentCss
430
+ if (markdownNode.parentClasses) {
431
+ const computedParentCss = markdownNode.parentClasses.map(
432
+ (_: any, index: number) =>
433
+ ctx.getNodeClasses(liveNode.id, 'auto', index)
434
+ );
435
+ if (
436
+ JSON.stringify(markdownNode.parentCss) !==
437
+ JSON.stringify(computedParentCss)
438
+ ) {
439
+ nextNode.parentCss = computedParentCss;
440
+ needsUpdate = true;
441
+ }
442
+ }
443
+
444
+ // gridCss
445
+ if (markdownNode.gridClasses) {
446
+ const [allClasses] = processClassesForViewports(
447
+ markdownNode.gridClasses,
448
+ {},
449
+ 1
450
+ );
451
+ if (allClasses && allClasses.length > 0) {
452
+ const computedGridCss = allClasses[0];
453
+ if (markdownNode.gridCss !== computedGridCss) {
454
+ nextNode.gridCss = computedGridCss;
455
+ needsUpdate = true;
456
+ }
457
+ }
458
+ }
459
+
460
+ if (needsUpdate) updatedNode = nextNode;
461
+ }
462
+ // Pattern 3: GridLayout Nodes -> parentCss & gridCss
463
+ else if (liveNode.nodeType === 'GridLayoutNode') {
464
+ const gridNode = liveNode as GridLayoutNode;
465
+ let needsUpdate = false;
466
+ const nextNode = { ...gridNode };
467
+
468
+ // parentCss
469
+ if (gridNode.parentClasses) {
470
+ const computedParentCss = gridNode.parentClasses.map(
471
+ (_: any, index: number) =>
472
+ ctx.getNodeClasses(liveNode.id, 'auto', index)
473
+ );
474
+ if (
475
+ JSON.stringify(gridNode.parentCss) !==
476
+ JSON.stringify(computedParentCss)
477
+ ) {
478
+ nextNode.parentCss = computedParentCss.join(` `);
479
+ needsUpdate = true;
480
+ }
481
+ }
482
+
483
+ // gridCss
484
+ if (gridNode.gridColumns) {
485
+ const { mobile, tablet, desktop } = gridNode.gridColumns;
486
+ let computedGridCss = `grid grid-cols-${mobile}`;
487
+ if (tablet !== mobile) {
488
+ computedGridCss += ` md:grid-cols-${tablet}`;
489
+ }
490
+ if (desktop !== tablet) {
491
+ computedGridCss += ` xl:grid-cols-${desktop}`;
492
+ }
493
+
494
+ if (gridNode.gridCss !== computedGridCss) {
495
+ nextNode.gridCss = computedGridCss;
496
+ needsUpdate = true;
497
+ }
498
+ }
499
+
500
+ if (needsUpdate) updatedNode = nextNode;
501
+ }
502
+
503
+ if (updatedNode) {
504
+ cookingUpdates.push(updatedNode);
505
+ }
506
+ } catch (e) {
507
+ console.warn(`Failed to cook node ${liveNode.id}`, e);
508
+ }
509
+ });
510
+
511
+ if (cookingUpdates.length > 0) {
512
+ ctx.modifyNodes(cookingUpdates, {
513
+ notify: false,
514
+ recordHistory: false,
515
+ });
516
+ addDebugMessage(`Cooked ${cookingUpdates.length} nodes.`);
517
+ }
518
+ setIsIndeterminateStage(false);
519
+ }
520
+
521
+ // --- PROCESSING STYLES ---
522
+ // Moved before SAVING_PANES to ensure the whitelist is generated from the cooked, exhaustive inventory
523
+ setStage('PROCESSING_STYLES');
524
+ setIsIndeterminateStage(true);
525
+ const baseFinalizationProgress =
526
+ PROGRESS_PHASES.PREPARATION +
527
+ PROGRESS_PHASES.UPLOADS +
528
+ PROGRESS_PHASES.PROCESSING;
529
+ setProgress(
530
+ baseFinalizationProgress + PROGRESS_PHASES.FINALIZATION / 2
531
+ );
532
+ addDebugMessage(`Processing styles...`);
533
+
534
+ try {
535
+ const { dirtyPaneIds, classes: dirtyClasses } =
536
+ ctx.getDirtyNodesClassData();
537
+
538
+ const astroEndpoint = `/api/tailwind`;
539
+ const astroPayload = { dirtyPaneIds, dirtyClasses };
540
+ const astroResponse = await fetch(astroEndpoint, {
541
+ method: 'POST',
542
+ headers: {
543
+ 'Content-Type': 'application/json',
544
+ 'X-Tenant-ID': tenantId,
545
+ },
546
+ credentials: 'include',
547
+ body: JSON.stringify(astroPayload),
548
+ });
549
+
550
+ if (!astroResponse.ok) {
551
+ throw new Error(
552
+ `CSS generation failed! status: ${astroResponse.status}`
553
+ );
554
+ }
555
+
556
+ const astroResult = await astroResponse.json();
557
+
558
+ if (!astroResult.success || !astroResult.generatedCss) {
559
+ throw new Error('CSS generation failed: no CSS returned');
560
+ }
561
+
562
+ addDebugMessage(
563
+ `CSS generated: ${astroResult.generatedCss.length} bytes for ${dirtyClasses.length} classes`
564
+ );
565
+
566
+ const goEndpoint = `${goBackend}/api/v1/tailwind/update`;
567
+ const goPayload = { frontendCss: astroResult.generatedCss };
568
+ const goResponse = await fetch(goEndpoint, {
569
+ method: 'POST',
570
+ headers: {
571
+ 'Content-Type': 'application/json',
572
+ 'X-Tenant-ID': tenantId,
573
+ },
574
+ credentials: 'include',
575
+ body: JSON.stringify(goPayload),
576
+ });
577
+
578
+ if (!goResponse.ok) {
579
+ throw new Error(`CSS save failed! status: ${goResponse.status}`);
580
+ }
581
+
582
+ const goResult = await goResponse.json();
583
+ addDebugMessage(
584
+ `CSS saved successfully: stylesVer ${goResult.stylesVer}`
585
+ );
586
+ } catch (error) {
587
+ const errorMsg =
588
+ error instanceof Error ? error.message : 'Unknown error';
589
+ addDebugMessage(`Styles processing failed: ${errorMsg}`);
590
+ throw new Error(`Failed to process styles: ${errorMsg}`);
591
+ } finally {
592
+ setIsIndeterminateStage(false);
593
+ }
594
+
595
+ // --- SAVING PANES ---
596
+ // Runs after styles to ensure DB gets minimal, correct payload
391
597
  if (dirtyPanes.length > 0) {
392
598
  setStage('SAVING_PANES');
393
599
  setIsIndeterminateStage(true);
@@ -400,6 +606,7 @@ export default function SaveModal({
400
606
  transformLivePaneForSave(ctx, paneNode.id, isContext)
401
607
  );
402
608
 
609
+ // Update context with minimal strings (idempotent, restoring runtime state)
403
610
  bulkPayload.forEach((payload) => {
404
611
  payload.optionsPayload.nodes.forEach((transformedNode) => {
405
612
  const liveNode = ctx.allNodes.get().get(transformedNode.id);
@@ -453,8 +660,6 @@ export default function SaveModal({
453
660
  `Processing ${dirtyPanes.length} panes via -> POST ${endpoint}`
454
661
  );
455
662
 
456
- //console.log(`bulkPayload`, bulkPayload)
457
-
458
663
  try {
459
664
  const response = await fetch(endpoint, {
460
665
  method: 'POST',
@@ -516,7 +721,7 @@ export default function SaveModal({
516
721
  const payload = await transformStoryFragmentForSave(
517
722
  ctx,
518
723
  fragment.id,
519
- window.TRACTSTACK_CONFIG?.tenantId || 'default'
724
+ tenantId
520
725
  );
521
726
 
522
727
  if (uploadedOGPaths[fragment.id]) {
@@ -583,14 +788,11 @@ export default function SaveModal({
583
788
  }
584
789
  }
585
790
 
586
- const baseFinalizationProgress =
587
- PROGRESS_PHASES.PREPARATION +
588
- PROGRESS_PHASES.UPLOADS +
589
- PROGRESS_PHASES.PROCESSING;
590
-
591
791
  if (dirtyPanes.length > 0) {
592
792
  setStage('LINKING_FILES');
593
793
  setIsIndeterminateStage(true);
794
+ // ... Linking files logic continues ...
795
+ // Note: Linking files remains after saving panes because it relies on panes existing in DB
594
796
  setProgress(baseFinalizationProgress);
595
797
  addDebugMessage('Starting file-pane relationship linking...');
596
798
 
@@ -644,74 +846,6 @@ export default function SaveModal({
644
846
  setIsIndeterminateStage(false);
645
847
  }
646
848
 
647
- setStage('PROCESSING_STYLES');
648
- setIsIndeterminateStage(true);
649
- setProgress(
650
- baseFinalizationProgress + PROGRESS_PHASES.FINALIZATION / 2
651
- );
652
- addDebugMessage(`Processing styles...`);
653
-
654
- try {
655
- const { dirtyPaneIds, classes: dirtyClasses } =
656
- ctx.getDirtyNodesClassData();
657
-
658
- const astroEndpoint = `/api/tailwind`;
659
- const astroPayload = { dirtyPaneIds, dirtyClasses };
660
- const astroResponse = await fetch(astroEndpoint, {
661
- method: 'POST',
662
- headers: {
663
- 'Content-Type': 'application/json',
664
- 'X-Tenant-ID': tenantId,
665
- },
666
- credentials: 'include',
667
- body: JSON.stringify(astroPayload),
668
- });
669
-
670
- if (!astroResponse.ok) {
671
- throw new Error(
672
- `CSS generation failed! status: ${astroResponse.status}`
673
- );
674
- }
675
-
676
- const astroResult = await astroResponse.json();
677
-
678
- if (!astroResult.success || !astroResult.generatedCss) {
679
- throw new Error('CSS generation failed: no CSS returned');
680
- }
681
-
682
- addDebugMessage(
683
- `CSS generated: ${astroResult.generatedCss.length} bytes for ${dirtyClasses.length} classes`
684
- );
685
-
686
- const goEndpoint = `${goBackend}/api/v1/tailwind/update`;
687
- const goPayload = { frontendCss: astroResult.generatedCss };
688
- const goResponse = await fetch(goEndpoint, {
689
- method: 'POST',
690
- headers: {
691
- 'Content-Type': 'application/json',
692
- 'X-Tenant-ID': tenantId,
693
- },
694
- credentials: 'include',
695
- body: JSON.stringify(goPayload),
696
- });
697
-
698
- if (!goResponse.ok) {
699
- throw new Error(`CSS save failed! status: ${goResponse.status}`);
700
- }
701
-
702
- const goResult = await goResponse.json();
703
- addDebugMessage(
704
- `CSS saved successfully: stylesVer ${goResult.stylesVer}`
705
- );
706
- } catch (error) {
707
- const errorMsg =
708
- error instanceof Error ? error.message : 'Unknown error';
709
- addDebugMessage(`Styles processing failed: ${errorMsg}`);
710
- throw new Error(`Failed to process styles: ${errorMsg}`);
711
- } finally {
712
- setIsIndeterminateStage(false);
713
- }
714
-
715
849
  if (pendingHomePageSlug) {
716
850
  setStage('UPDATING_HOME_PAGE');
717
851
  setIsIndeterminateStage(true);
@@ -774,6 +908,30 @@ export default function SaveModal({
774
908
  }
775
909
  }
776
910
 
911
+ if (hydrate) {
912
+ addDebugMessage('Finalizing setup (Kill Switch)...');
913
+ try {
914
+ const response = await fetch(`${goBackend}/api/v1/setup/complete`, {
915
+ method: 'POST',
916
+ headers: {
917
+ 'Content-Type': 'application/json',
918
+ 'X-Tenant-ID': tenantId,
919
+ },
920
+ credentials: 'include',
921
+ body: JSON.stringify({}),
922
+ });
923
+
924
+ if (!response.ok) {
925
+ throw new Error(`Kill Switch failed: ${response.status}`);
926
+ }
927
+ addDebugMessage('Hydration token cleared.');
928
+ } catch (e) {
929
+ console.error('Kill switch error:', e);
930
+ // We don't throw here to ensure the user still gets to the dashboard
931
+ addDebugMessage('Warning: Failed to clear hydration token.');
932
+ }
933
+ }
934
+
777
935
  setStage('COMPLETED');
778
936
  setProgress(100);
779
937
  addDebugMessage('Save process completed successfully!');
@@ -822,6 +980,9 @@ export default function SaveModal({
822
980
  case 'PROCESSING_OG_IMAGES':
823
981
  description = `Processing social images${getProgressText()}`;
824
982
  break;
983
+ case 'COOKING_NODES':
984
+ description = 'Preparing content styles...';
985
+ break;
825
986
  case 'SAVING_PANES':
826
987
  description = `${actionText} pane content...`;
827
988
  break;
@@ -1031,7 +1192,17 @@ export default function SaveModal({
1031
1192
 
1032
1193
  {(stage === 'COMPLETED' || stage === 'ERROR') && (
1033
1194
  <div className="flex justify-end gap-2">
1034
- {stage === 'COMPLETED' && (
1195
+ {hydrate && stage === 'COMPLETED' && (
1196
+ <button
1197
+ onClick={() =>
1198
+ (window.location.href = '/storykeep/branding')
1199
+ }
1200
+ className={`rounded bg-cyan-600 px-4 py-2 text-white transition-colors hover:bg-cyan-700`}
1201
+ >
1202
+ Continue
1203
+ </button>
1204
+ )}
1205
+ {!hydrate && stage === 'COMPLETED' && (
1035
1206
  <>
1036
1207
  <a
1037
1208
  href={visitPageUrl}