@optimizely/ocp-local-env 1.0.0-beta.9 → 1.0.1

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 (185) hide show
  1. package/README.md +18 -133
  2. package/dist/package.json +15 -9
  3. package/dist/public/bundle.fa87c838198caf8c051a.js +3 -0
  4. package/dist/public/{bundle.82dc5d29fffb9f205051.js.LICENSE.txt → bundle.fa87c838198caf8c051a.js.LICENSE.txt} +3 -3
  5. package/dist/public/bundle.fa87c838198caf8c051a.js.map +1 -0
  6. package/dist/public/index.html +1 -1
  7. package/dist/src/cli.js +2 -2
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/executor/FunctionExecutor.js +8 -5
  10. package/dist/src/executor/FunctionExecutor.js.map +1 -1
  11. package/dist/src/executor/JobExecutor.js +0 -17
  12. package/dist/src/executor/JobExecutor.js.map +1 -1
  13. package/dist/src/executor/LifecycleExecutor.js +11 -2
  14. package/dist/src/executor/LifecycleExecutor.js.map +1 -1
  15. package/dist/src/executor/watcher.d.ts +6 -0
  16. package/dist/src/executor/watcher.js +45 -0
  17. package/dist/src/executor/watcher.js.map +1 -1
  18. package/dist/src/local_engine/LocalJobApi.d.ts +31 -2
  19. package/dist/src/local_engine/LocalJobApi.js +42 -17
  20. package/dist/src/local_engine/LocalJobApi.js.map +1 -1
  21. package/dist/src/local_engine/LocalNotifier.d.ts +3 -2
  22. package/dist/src/local_engine/LocalNotifier.js +7 -0
  23. package/dist/src/local_engine/LocalNotifier.js.map +1 -1
  24. package/dist/src/local_engine/LocalSourceApi.d.ts +40 -0
  25. package/dist/src/local_engine/LocalSourceApi.js +69 -0
  26. package/dist/src/local_engine/LocalSourceApi.js.map +1 -0
  27. package/dist/src/local_engine/local-engine-child-base.d.ts +0 -12
  28. package/dist/src/local_engine/local-engine-child-base.js +0 -127
  29. package/dist/src/local_engine/local-engine-child-base.js.map +1 -1
  30. package/dist/src/local_engine/local-engine-client.d.ts +19 -29
  31. package/dist/src/local_engine/local-engine-client.js +45 -94
  32. package/dist/src/local_engine/local-engine-client.js.map +1 -1
  33. package/dist/src/local_engine/local-engine-types.d.ts +13 -50
  34. package/dist/src/local_engine/local-engine-unified.d.ts +6 -13
  35. package/dist/src/local_engine/local-engine-unified.js +108 -222
  36. package/dist/src/local_engine/local-engine-unified.js.map +1 -1
  37. package/dist/src/local_engine/localSDKConfig.d.ts +6 -0
  38. package/dist/src/local_engine/localSDKConfig.js +66 -1
  39. package/dist/src/local_engine/localSDKConfig.js.map +1 -1
  40. package/dist/src/local_engine/storage/BaseKVStoreWrapper.d.ts +1 -2
  41. package/dist/src/local_engine/storage/BaseKVStoreWrapper.js.map +1 -1
  42. package/dist/src/local_engine/storage/LocalConfigStore.d.ts +7 -13
  43. package/dist/src/local_engine/storage/LocalConfigStore.js +9 -31
  44. package/dist/src/local_engine/storage/LocalConfigStore.js.map +1 -1
  45. package/dist/src/local_engine/storage/LocalKVStore.d.ts +4 -0
  46. package/dist/src/local_engine/storage/LocalKVStore.js +23 -0
  47. package/dist/src/local_engine/storage/LocalKVStore.js.map +1 -1
  48. package/dist/src/local_engine/storage/SourceDataStore.d.ts +16 -14
  49. package/dist/src/local_engine/storage/SourceDataStore.js +57 -57
  50. package/dist/src/local_engine/storage/SourceDataStore.js.map +1 -1
  51. package/dist/src/local_engine/utils.d.ts +2 -0
  52. package/dist/src/local_engine/utils.js +4 -2
  53. package/dist/src/local_engine/utils.js.map +1 -1
  54. package/dist/src/server/api/deploy.d.ts +20 -0
  55. package/dist/src/server/api/deploy.js +399 -0
  56. package/dist/src/server/api/deploy.js.map +1 -0
  57. package/dist/src/server/api/destinations.js +1 -1
  58. package/dist/src/server/api/destinations.js.map +1 -1
  59. package/dist/src/server/api/env.d.ts +2 -0
  60. package/dist/src/server/api/env.js +28 -0
  61. package/dist/src/server/api/env.js.map +1 -0
  62. package/dist/src/server/api/functions.js +13 -12
  63. package/dist/src/server/api/functions.js.map +1 -1
  64. package/dist/src/server/api/opalTools.d.ts +4 -0
  65. package/dist/src/server/api/opalTools.js +81 -0
  66. package/dist/src/server/api/opalTools.js.map +1 -0
  67. package/dist/src/server/api/schema.d.ts +6 -0
  68. package/dist/src/server/api/schema.js +95 -0
  69. package/dist/src/server/api/schema.js.map +1 -0
  70. package/dist/src/server/api/sources.d.ts +1 -2
  71. package/dist/src/server/api/sources.js +16 -275
  72. package/dist/src/server/api/sources.js.map +1 -1
  73. package/dist/src/server/api/v1.js +63 -55
  74. package/dist/src/server/api/v1.js.map +1 -1
  75. package/dist/src/server/api.d.ts +4 -0
  76. package/dist/src/server/api.js +131 -9
  77. package/dist/src/server/api.js.map +1 -1
  78. package/dist/src/server/app-discovery.js +37 -4
  79. package/dist/src/server/app-discovery.js.map +1 -1
  80. package/dist/src/server/app-env.d.ts +21 -0
  81. package/dist/src/server/app-env.js +122 -0
  82. package/dist/src/server/app-env.js.map +1 -0
  83. package/dist/src/server/browserFocus.d.ts +5 -0
  84. package/dist/src/server/browserFocus.js +39 -0
  85. package/dist/src/server/browserFocus.js.map +1 -0
  86. package/dist/src/server/config.d.ts +3 -0
  87. package/dist/src/server/config.js +7 -0
  88. package/dist/src/server/config.js.map +1 -1
  89. package/dist/src/server/mockDataGenerator.d.ts +1 -2
  90. package/dist/src/server/mockDataGenerator.js +77 -35
  91. package/dist/src/server/mockDataGenerator.js.map +1 -1
  92. package/dist/src/server/mockToolDataGenerator.d.ts +12 -0
  93. package/dist/src/server/mockToolDataGenerator.js +393 -0
  94. package/dist/src/server/mockToolDataGenerator.js.map +1 -0
  95. package/dist/src/server.js +152 -168
  96. package/dist/src/server.js.map +1 -1
  97. package/dist/src/types/app.d.ts +1 -3
  98. package/dist/src/types/app.js.map +1 -1
  99. package/dist/src/ui/components/App.js +303 -47
  100. package/dist/src/ui/components/App.js.map +1 -1
  101. package/dist/src/ui/components/DeployModal.d.ts +16 -0
  102. package/dist/src/ui/components/DeployModal.js +75 -0
  103. package/dist/src/ui/components/DeployModal.js.map +1 -0
  104. package/dist/src/ui/components/DestinationBatchEditor.d.ts +9 -1
  105. package/dist/src/ui/components/DestinationBatchEditor.js +12 -4
  106. package/dist/src/ui/components/DestinationBatchEditor.js.map +1 -1
  107. package/dist/src/ui/components/DestinationsView.js +5 -1
  108. package/dist/src/ui/components/DestinationsView.js.map +1 -1
  109. package/dist/src/ui/components/EntityNotAvailable.d.ts +15 -0
  110. package/dist/src/ui/components/EntityNotAvailable.js +27 -0
  111. package/dist/src/ui/components/EntityNotAvailable.js.map +1 -0
  112. package/dist/src/ui/components/EnvViewer.d.ts +8 -0
  113. package/dist/src/ui/components/EnvViewer.js +35 -0
  114. package/dist/src/ui/components/EnvViewer.js.map +1 -0
  115. package/dist/src/ui/components/FunctionsView.d.ts +2 -6
  116. package/dist/src/ui/components/FunctionsView.js +52 -75
  117. package/dist/src/ui/components/FunctionsView.js.map +1 -1
  118. package/dist/src/ui/components/JobsView.js +4 -4
  119. package/dist/src/ui/components/JobsView.js.map +1 -1
  120. package/dist/src/ui/components/KVStoreViewer.js +5 -5
  121. package/dist/src/ui/components/KVStoreViewer.js.map +1 -1
  122. package/dist/src/ui/components/KeyValueEditor.d.ts +15 -0
  123. package/dist/src/ui/components/KeyValueEditor.js +12 -0
  124. package/dist/src/ui/components/KeyValueEditor.js.map +1 -0
  125. package/dist/src/ui/components/NotificationViewer.d.ts +2 -11
  126. package/dist/src/ui/components/NotificationViewer.js +69 -15
  127. package/dist/src/ui/components/NotificationViewer.js.map +1 -1
  128. package/dist/src/ui/components/OdpSchemaView.d.ts +3 -0
  129. package/dist/src/ui/components/OdpSchemaView.js +58 -0
  130. package/dist/src/ui/components/OdpSchemaView.js.map +1 -0
  131. package/dist/src/ui/components/OpalToolsView.d.ts +9 -0
  132. package/dist/src/ui/components/OpalToolsView.js +399 -0
  133. package/dist/src/ui/components/OpalToolsView.js.map +1 -0
  134. package/dist/src/ui/components/ResponseViewer.d.ts +39 -0
  135. package/dist/src/ui/components/ResponseViewer.js +43 -0
  136. package/dist/src/ui/components/ResponseViewer.js.map +1 -0
  137. package/dist/src/ui/components/SecretsStoreViewer.js +3 -2
  138. package/dist/src/ui/components/SecretsStoreViewer.js.map +1 -1
  139. package/dist/src/ui/components/SettingsStoreViewer.js +3 -2
  140. package/dist/src/ui/components/SettingsStoreViewer.js.map +1 -1
  141. package/dist/src/ui/components/SourceEmittedDataPanel.d.ts +8 -0
  142. package/dist/src/ui/components/SourceEmittedDataPanel.js +103 -0
  143. package/dist/src/ui/components/SourceEmittedDataPanel.js.map +1 -0
  144. package/dist/src/ui/components/SourcesView.js +4 -8
  145. package/dist/src/ui/components/SourcesView.js.map +1 -1
  146. package/dist/src/ui/components/StoreViewer.js +2 -1
  147. package/dist/src/ui/components/StoreViewer.js.map +1 -1
  148. package/dist/src/ui/components/TabbedConsole.d.ts +11 -0
  149. package/dist/src/ui/components/TabbedConsole.js +134 -44
  150. package/dist/src/ui/components/TabbedConsole.js.map +1 -1
  151. package/dist/src/ui/components/ToolSettingsView.d.ts +7 -0
  152. package/dist/src/ui/components/ToolSettingsView.js +25 -0
  153. package/dist/src/ui/components/ToolSettingsView.js.map +1 -0
  154. package/dist/src/ui/components/common/EyeIcon.js +2 -1
  155. package/dist/src/ui/components/common/EyeIcon.js.map +1 -1
  156. package/dist/src/ui/hooks/useKeyValuePairs.d.ts +52 -0
  157. package/dist/src/ui/hooks/useKeyValuePairs.js +66 -0
  158. package/dist/src/ui/hooks/useKeyValuePairs.js.map +1 -0
  159. package/dist/src/ui/store/formStateSlice.d.ts +19 -2
  160. package/dist/src/ui/store/formStateSlice.js +69 -4
  161. package/dist/src/ui/store/formStateSlice.js.map +1 -1
  162. package/dist/src/ui/types/common.d.ts +33 -0
  163. package/dist/src/ui/types/common.js +6 -0
  164. package/dist/src/ui/types/common.js.map +1 -0
  165. package/package.json +15 -9
  166. package/dist/public/bundle.82dc5d29fffb9f205051.js +0 -3
  167. package/dist/public/bundle.82dc5d29fffb9f205051.js.map +0 -1
  168. package/dist/src/executor/SourceExecutor.d.ts +0 -32
  169. package/dist/src/executor/SourceExecutor.js +0 -163
  170. package/dist/src/executor/SourceExecutor.js.map +0 -1
  171. package/dist/src/local_engine/storage/SourceJobExecutionStore.d.ts +0 -25
  172. package/dist/src/local_engine/storage/SourceJobExecutionStore.js +0 -61
  173. package/dist/src/local_engine/storage/SourceJobExecutionStore.js.map +0 -1
  174. package/dist/src/ui/components/SourceDataViewer.d.ts +0 -8
  175. package/dist/src/ui/components/SourceDataViewer.js +0 -84
  176. package/dist/src/ui/components/SourceDataViewer.js.map +0 -1
  177. package/dist/src/ui/components/SourceJobsSection.d.ts +0 -8
  178. package/dist/src/ui/components/SourceJobsSection.js +0 -99
  179. package/dist/src/ui/components/SourceJobsSection.js.map +0 -1
  180. package/dist/src/ui/components/SourceLifecycleSection.d.ts +0 -7
  181. package/dist/src/ui/components/SourceLifecycleSection.js +0 -58
  182. package/dist/src/ui/components/SourceLifecycleSection.js.map +0 -1
  183. package/dist/src/ui/components/SourceWebhookEditor.d.ts +0 -8
  184. package/dist/src/ui/components/SourceWebhookEditor.js +0 -181
  185. package/dist/src/ui/components/SourceWebhookEditor.js.map +0 -1
@@ -11,8 +11,13 @@ const JobsView_1 = __importDefault(require("./JobsView"));
11
11
  const TabbedConsole_1 = __importDefault(require("./TabbedConsole"));
12
12
  const DestinationsView_1 = require("./DestinationsView");
13
13
  const SourcesView_1 = require("./SourcesView");
14
+ const OpalToolsView_1 = __importDefault(require("./OpalToolsView"));
15
+ const ToolSettingsView_1 = __importDefault(require("./ToolSettingsView"));
16
+ const OdpSchemaView_1 = __importDefault(require("./OdpSchemaView"));
14
17
  const useQueryParams_1 = require("../hooks/useQueryParams");
15
18
  const store_1 = require("../store");
19
+ const axiom_icons_1 = require("@optimizely/axiom-icons");
20
+ const DeployModal_1 = require("./DeployModal");
16
21
  const IFRAME_RESIZER_MESSAGES_PREFIX = "[iFrameSizer]app_directory_frame_directory";
17
22
  const IFRAME_RESIZER_READY_MESSAGE = `${IFRAME_RESIZER_MESSAGES_PREFIX}:8:false:false:32:true:true:null:bodyOffset:null:null:0:false:parent:scroll:true`;
18
23
  // Ex.: [iFrameSizer]app_directory_frame_directory:7096:1955:mutationObserver
@@ -42,11 +47,27 @@ const App = () => {
42
47
  const [buildStatus, setBuildStatus] = (0, react_1.useState)(null);
43
48
  const [storeTimestamps, setStoreTimestamps] = (0, react_1.useState)(null);
44
49
  const [appInstalled, setAppInstalled] = (0, react_1.useState)(false);
50
+ // Install version counter - increments on each install to force UI refresh
51
+ const [installVersion, setInstallVersion] = (0, react_1.useState)(0);
45
52
  const [notificationCount, setNotificationCount] = (0, react_1.useState)(0);
46
53
  const [config, setConfig] = (0, react_1.useState)({
47
54
  showZaiusAdminSections: false
48
55
  });
49
- const [bannerVisible, setBannerVisible] = (0, react_1.useState)(true);
56
+ const [permanentlyDisconnected, setPermanentlyDisconnected] = (0, react_1.useState)(false);
57
+ const [appChanged, setAppChanged] = (0, react_1.useState)(null);
58
+ const [sourceDataVersion, setSourceDataVersion] = (0, react_1.useState)(0);
59
+ const [sourceDataTotalEmitted, setSourceDataTotalEmitted] = (0, react_1.useState)(0);
60
+ const [notificationVersion, setNotificationVersion] = (0, react_1.useState)(0);
61
+ const [envVersion, setEnvVersion] = (0, react_1.useState)(0);
62
+ const [deployStatus, setDeployStatus] = (0, react_1.useState)({ deployInProgress: false, lastDeploySuccess: null });
63
+ const [deployLogs, setDeployLogs] = (0, react_1.useState)([]);
64
+ const [deployVersion, setDeployVersion] = (0, react_1.useState)(0);
65
+ const [showDeployModal, setShowDeployModal] = (0, react_1.useState)(false);
66
+ const [deployModalData, setDeployModalData] = (0, react_1.useState)(null);
67
+ // Reference to store disconnect timer for 30-second timeout
68
+ const disconnectTimerRef = (0, react_1.useRef)(null);
69
+ // Reference to store initial app info for change detection
70
+ const initialAppInfoRef = (0, react_1.useRef)(null);
50
71
  // Initialize Redux store with app name when appInfo is available
51
72
  (0, react_1.useEffect)(() => {
52
73
  if (appInfo?.name) {
@@ -55,9 +76,10 @@ const App = () => {
55
76
  }, [appInfo?.name]);
56
77
  // Reference to store polling interval IDs
57
78
  const pollingIntervals = (0, react_1.useRef)({
58
- logs: null,
59
79
  status: null,
60
80
  });
81
+ // Reference to store EventSource for cleanup on page unload
82
+ const eventSourceRef = (0, react_1.useRef)(null);
61
83
  // Reference to the settings iframe
62
84
  const settingsIframeRef = (0, react_1.useRef)(null);
63
85
  const setIframeRef = (iframe) => {
@@ -83,7 +105,38 @@ const App = () => {
83
105
  }
84
106
  }
85
107
  })}`;
86
- settingsIframeRef.current.contentWindow.postMessage(userFeaturesMessage, "http://localhost:3000");
108
+ settingsIframeRef.current.contentWindow.postMessage(userFeaturesMessage, window.location.origin);
109
+ // Send prismContext with mock organization ID to prevent "Organization ID is missing" errors
110
+ const prismContextMessage = `[iFrameSizer]message:${JSON.stringify({
111
+ method: 'prismContext',
112
+ params: {
113
+ productName: 'local-dev',
114
+ subProductName: 'ocp-local-env',
115
+ trackerId: 'local-development-tracker-id',
116
+ scope: 1,
117
+ originalUser: {
118
+ id: 1,
119
+ email: 'local-dev@example.com',
120
+ name: 'Local Developer',
121
+ isOptiId: false,
122
+ isInternalUser: true,
123
+ optiIdAccessToken: 'local-dev-mock-token'
124
+ },
125
+ organization: {
126
+ id: 1,
127
+ name: 'Local Development Org',
128
+ masterCustomerId: 'local-master-customer',
129
+ turnstileOrganizationId: 'local-test-org'
130
+ },
131
+ account: {
132
+ id: 1,
133
+ name: 'Local Development Account',
134
+ instanceId: 'local-test-instance',
135
+ isOptiHub: false
136
+ }
137
+ }
138
+ })}`;
139
+ settingsIframeRef.current.contentWindow.postMessage(prismContextMessage, window.location.origin);
87
140
  console.log("Sent ready event to iframe after 2 second delay");
88
141
  }
89
142
  }, 2000);
@@ -108,15 +161,19 @@ const App = () => {
108
161
  };
109
162
  // Set up polling on component mount
110
163
  (0, react_1.useEffect)(() => {
111
- // Initial data load
164
+ // Don't set up new connections if permanently disconnected
165
+ if (permanentlyDisconnected) {
166
+ return;
167
+ }
168
+ // Initial data load (logs come via SSE initialLogs event)
112
169
  fetchConfig();
113
170
  fetchAppInfo();
114
171
  fetchStatus();
115
- fetchConsoleLogs();
116
172
  fetchInstallationStatus();
117
173
  fetchNotificationCount();
118
174
  // Set up SSE connection for real-time build status updates
119
175
  const eventSource = new EventSource('/devserver/api/events');
176
+ eventSourceRef.current = eventSource;
120
177
  eventSource.onmessage = (event) => {
121
178
  const data = JSON.parse(event.data);
122
179
  switch (data.type) {
@@ -130,6 +187,10 @@ const App = () => {
130
187
  lastBuildTimestamp: null,
131
188
  lastBuildError: null
132
189
  });
190
+ // Reset deploy success status on code change (build start)
191
+ if (!deployStatus.deployInProgress) {
192
+ setDeployStatus(prev => ({ ...prev, lastDeploySuccess: null }));
193
+ }
133
194
  break;
134
195
  case 'buildSuccess':
135
196
  setBuildStatus({
@@ -138,8 +199,7 @@ const App = () => {
138
199
  lastBuildTimestamp: data.timestamp,
139
200
  lastBuildError: null
140
201
  });
141
- // Refresh logs and store timestamps after build
142
- fetchConsoleLogs();
202
+ // Refresh store timestamps after build (logs come via SSE)
143
203
  fetchStatus();
144
204
  break;
145
205
  case 'buildError':
@@ -149,7 +209,6 @@ const App = () => {
149
209
  lastBuildTimestamp: data.timestamp,
150
210
  lastBuildError: data.error
151
211
  });
152
- fetchConsoleLogs();
153
212
  break;
154
213
  case 'status':
155
214
  // Initial status on connection
@@ -160,10 +219,112 @@ const App = () => {
160
219
  lastBuildError: data.lastBuildError
161
220
  });
162
221
  break;
222
+ case 'log':
223
+ // Append new log, keep last 500
224
+ setLogs(prev => [...prev.slice(-499), data.entry]);
225
+ break;
226
+ case 'initialLogs':
227
+ setLogs(data.logs || []);
228
+ // Handle app change detection (only compare app names, not paths)
229
+ if (data.app && data.app.name) {
230
+ if (!initialAppInfoRef.current) {
231
+ initialAppInfoRef.current = { name: data.app.name, path: data.app.path };
232
+ }
233
+ else if (initialAppInfoRef.current.name !== data.app.name) {
234
+ setAppChanged({ oldApp: initialAppInfoRef.current.name, newApp: data.app.name });
235
+ }
236
+ }
237
+ break;
238
+ case 'logsCleared':
239
+ setLogs([]);
240
+ break;
241
+ case 'serverShutdown':
242
+ // Server is shutting down - immediately show disconnect overlay
243
+ eventSource.close();
244
+ eventSourceRef.current = null;
245
+ setPermanentlyDisconnected(true);
246
+ // Update tab title to indicate disconnection
247
+ document.title = '[DISCONNECTED] ' + document.title;
248
+ break;
249
+ case 'connectionEvicted':
250
+ // Connection evicted due to limit - show disconnect overlay
251
+ eventSource.close();
252
+ eventSourceRef.current = null;
253
+ setPermanentlyDisconnected(true);
254
+ // Update tab title to indicate disconnection
255
+ document.title = '[DISCONNECTED] ' + document.title;
256
+ break;
257
+ case 'sourceDataChanged':
258
+ // Source data was emitted — increment version to trigger re-fetch in panel
259
+ setSourceDataVersion(v => v + 1);
260
+ setSourceDataTotalEmitted(data.totalEmitted ?? 0);
261
+ break;
262
+ case 'notificationChanged':
263
+ // Notification was created — increment version to trigger re-fetch in panel
264
+ setNotificationVersion(v => v + 1);
265
+ fetchNotificationCount();
266
+ break;
267
+ case 'envChanged':
268
+ setEnvVersion(v => v + 1);
269
+ break;
270
+ case 'installSuccess':
271
+ // App was installed via API - update state and increment version
272
+ console.log('SSE: App installed via API');
273
+ setAppInstalled(true);
274
+ setInstallVersion(v => v + 1); // Increment to force UI refresh
275
+ setConnected(true);
276
+ break;
277
+ case 'uninstallSuccess':
278
+ // App was uninstalled via API - update state
279
+ console.log('SSE: App uninstalled via API');
280
+ setAppInstalled(false);
281
+ setConnected(false);
282
+ break;
283
+ case 'deployStart':
284
+ setDeployStatus({ deployInProgress: true, lastDeploySuccess: null });
285
+ // Add a separator between deploy runs instead of clearing logs
286
+ setDeployLogs(prev => prev.length > 0
287
+ ? [...prev, '───────────────────────────────────────']
288
+ : prev);
289
+ setDeployVersion(v => v + 1);
290
+ break;
291
+ case 'deployLog':
292
+ setDeployLogs(prev => [...prev, data.line]);
293
+ setDeployVersion(v => v + 1);
294
+ break;
295
+ case 'deploySuccess':
296
+ setDeployStatus({ deployInProgress: false, lastDeploySuccess: true });
297
+ setDeployVersion(v => v + 1);
298
+ // Refresh app info to update version display in nav
299
+ fetchAppInfo();
300
+ break;
301
+ case 'deployError':
302
+ setDeployStatus({ deployInProgress: false, lastDeploySuccess: false });
303
+ if (data.error) {
304
+ setDeployLogs(prev => [...prev, `Error: ${data.error}`]);
305
+ }
306
+ setDeployVersion(v => v + 1);
307
+ break;
308
+ case 'deployStatus':
309
+ // Initial deploy status on connection
310
+ setDeployStatus({
311
+ deployInProgress: data.deployInProgress || false,
312
+ lastDeploySuccess: data.lastDeploySuccess ?? null
313
+ });
314
+ break;
163
315
  }
164
316
  };
165
317
  eventSource.onerror = () => {
166
318
  setConnected(false);
319
+ // Start 30-second timer if not already started
320
+ if (!disconnectTimerRef.current && !permanentlyDisconnected) {
321
+ disconnectTimerRef.current = setTimeout(() => {
322
+ // Permanently disconnect - close EventSource and show message
323
+ eventSource.close();
324
+ eventSourceRef.current = null;
325
+ setPermanentlyDisconnected(true);
326
+ }, 30000);
327
+ }
167
328
  // Fallback to polling if SSE fails
168
329
  if (!pollingIntervals.current.status) {
169
330
  pollingIntervals.current.status = setInterval(fetchStatus, 3000);
@@ -171,25 +332,43 @@ const App = () => {
171
332
  };
172
333
  eventSource.onopen = () => {
173
334
  setConnected(true);
335
+ // Clear disconnect timer if we reconnected in time
336
+ if (disconnectTimerRef.current) {
337
+ clearTimeout(disconnectTimerRef.current);
338
+ disconnectTimerRef.current = null;
339
+ }
340
+ // Check if app has changed (server restarted with different app)
341
+ // Only check if we had a previous app loaded
342
+ if (initialAppInfoRef.current) {
343
+ fetchAppInfo(true);
344
+ }
174
345
  // Stop fallback polling if it was started
175
346
  if (pollingIntervals.current.status) {
176
347
  clearInterval(pollingIntervals.current.status);
177
348
  pollingIntervals.current.status = null;
178
349
  }
179
350
  };
180
- // Keep log polling (can be extended to SSE later if needed)
181
- pollingIntervals.current.logs = setInterval(fetchConsoleLogs, 2000);
351
+ // Close SSE on page unload/refresh to prevent connection accumulation
352
+ // This is critical for Firefox which has a strict 6-connection limit per domain
353
+ const handleBeforeUnload = () => {
354
+ if (eventSourceRef.current) {
355
+ eventSourceRef.current.close();
356
+ }
357
+ };
358
+ window.addEventListener('beforeunload', handleBeforeUnload);
182
359
  // Cleanup
183
360
  return () => {
184
- eventSource.close();
185
- if (pollingIntervals.current.logs) {
186
- clearInterval(pollingIntervals.current.logs);
361
+ if (disconnectTimerRef.current) {
362
+ clearTimeout(disconnectTimerRef.current);
187
363
  }
364
+ eventSource.close();
365
+ eventSourceRef.current = null;
366
+ window.removeEventListener('beforeunload', handleBeforeUnload);
188
367
  if (pollingIntervals.current.status) {
189
368
  clearInterval(pollingIntervals.current.status);
190
369
  }
191
370
  };
192
- }, []);
371
+ }, [permanentlyDisconnected]);
193
372
  // Listen for events from the iframe
194
373
  (0, react_1.useEffect)(() => {
195
374
  const deserializeResizerMessage = (data) => {
@@ -223,11 +402,11 @@ const App = () => {
223
402
  const message = deserializeResizerMessage(event.data);
224
403
  // Handle getTracker message
225
404
  if (message.method === "getTracker") {
226
- const message = serializeResizerMessage({
405
+ const response = serializeResizerMessage({
227
406
  method: "setTracker",
228
407
  params: "local-development-tracker-id",
229
408
  });
230
- settingsIframeRef.current?.contentWindow?.postMessage(message, event.origin);
409
+ settingsIframeRef.current?.contentWindow?.postMessage(response, event.origin);
231
410
  console.log("getTracker sent");
232
411
  }
233
412
  // Handle installation lifecycle events
@@ -238,6 +417,7 @@ const App = () => {
238
417
  case "install_success":
239
418
  console.log("Installation successful, updating app state");
240
419
  setAppInstalled(true);
420
+ setInstallVersion(v => v + 1); // Increment to force UI refresh
241
421
  setConnected(true);
242
422
  break;
243
423
  case "uninstall_success":
@@ -269,11 +449,25 @@ const App = () => {
269
449
  // eslint-disable-next-line react-hooks/exhaustive-deps
270
450
  }, []);
271
451
  // Fetch app info from the API
272
- const fetchAppInfo = async () => {
452
+ const fetchAppInfo = async (checkForChanges = false) => {
273
453
  try {
274
454
  const response = await fetch("/devserver/api/app");
275
455
  if (response.ok) {
276
456
  const data = await response.json();
457
+ // Store initial app info on first successful fetch
458
+ if (!initialAppInfoRef.current && data.name) {
459
+ initialAppInfoRef.current = { name: data.name, path: data.path };
460
+ }
461
+ // Check if app has changed (different app being served) - only compare names
462
+ if (checkForChanges && initialAppInfoRef.current && data.name) {
463
+ if (initialAppInfoRef.current.name !== data.name) {
464
+ setAppChanged({
465
+ oldApp: initialAppInfoRef.current.name,
466
+ newApp: data.name
467
+ });
468
+ return; // Don't update appInfo - show the change notification instead
469
+ }
470
+ }
277
471
  setAppInfo(data);
278
472
  setConnected(true);
279
473
  }
@@ -307,34 +501,11 @@ const App = () => {
307
501
  setConnected(false);
308
502
  }
309
503
  };
310
- // Fetch console logs from the API
311
- const fetchConsoleLogs = async () => {
312
- try {
313
- const response = await fetch("/devserver/api/console/logs?limit=50");
314
- if (response.ok) {
315
- const data = await response.json();
316
- if (data.logs && data.logs.length > 0) {
317
- setLogs(data.logs); // Pass raw LogEntry[] directly
318
- }
319
- else {
320
- setLogs([]);
321
- }
322
- setConnected(true);
323
- }
324
- else {
325
- setConnected(false);
326
- }
327
- }
328
- catch (error) {
329
- console.error("Error fetching console logs:", error);
330
- setConnected(false);
331
- }
332
- };
333
504
  // Clear console logs
334
505
  const clearConsoleLogs = async () => {
335
506
  try {
336
507
  await fetch("/devserver/api/console/logs", { method: "DELETE" });
337
- fetchConsoleLogs();
508
+ // SSE logsCleared event will update the UI
338
509
  }
339
510
  catch (error) {
340
511
  console.error("Error clearing logs:", error);
@@ -392,9 +563,8 @@ const App = () => {
392
563
  },
393
564
  });
394
565
  if (response.ok) {
395
- // Immediately fetch new status and logs
566
+ // Immediately fetch new status (logs come via SSE)
396
567
  fetchStatus();
397
- fetchConsoleLogs();
398
568
  }
399
569
  else {
400
570
  console.error("Failed to trigger build");
@@ -404,14 +574,100 @@ const App = () => {
404
574
  console.error("Error triggering build:", error);
405
575
  }
406
576
  };
407
- return ((0, jsx_runtime_1.jsxs)("div", { className: "app postman-layout", children: [bannerVisible && ((0, jsx_runtime_1.jsxs)("div", { className: "beta-banner", children: [(0, jsx_runtime_1.jsx)("span", { className: "beta-banner-icon", children: "\u26A0\uFE0F" }), (0, jsx_runtime_1.jsxs)("span", { className: "beta-banner-text", children: [(0, jsx_runtime_1.jsx)("strong", { children: "BETA:" }), " This tool is in beta - we welcome your", ' ', (0, jsx_runtime_1.jsx)("a", { href: "https://github.com/optimizely/ocp-developer-feedback/issues/new/choose", target: "_blank", rel: "noopener noreferrer", className: "beta-banner-link", children: "feedback" })] }), (0, jsx_runtime_1.jsx)("button", { className: "beta-banner-close", onClick: () => setBannerVisible(false), "aria-label": "Close banner", children: "\u2715" })] })), (0, jsx_runtime_1.jsxs)("header", { className: "top-nav", children: [(0, jsx_runtime_1.jsxs)("div", { className: "nav-left", children: [(0, jsx_runtime_1.jsx)("div", { className: "app-title", children: (0, jsx_runtime_1.jsx)("h1", { children: "OCP Local Environment" }) }), appInfo && ((0, jsx_runtime_1.jsxs)("div", { className: "app-info-nav", children: [(0, jsx_runtime_1.jsx)("span", { className: "app-name", children: appInfo.displayName || appInfo.name }), (0, jsx_runtime_1.jsxs)("span", { className: "app-version", children: ["v", appInfo.version] })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "nav-right", children: [(0, jsx_runtime_1.jsxs)("button", { className: `rebuild-btn ${buildStatus?.buildInProgress ? 'building' :
577
+ // Compute deploy button CSS modifier class based on current deploy status
578
+ const getDeployStatusClass = () => {
579
+ if (deployStatus.deployInProgress)
580
+ return 'deploying';
581
+ if (deployStatus.lastDeploySuccess === true)
582
+ return 'deploy-success';
583
+ if (deployStatus.lastDeploySuccess === false)
584
+ return 'deploy-error';
585
+ return '';
586
+ };
587
+ // Append a deploy log message and increment the version counter
588
+ const appendDeployLog = (message) => {
589
+ setDeployLogs(prev => [...prev, message]);
590
+ setDeployVersion(v => v + 1);
591
+ };
592
+ // Trigger a deploy
593
+ const triggerDeploy = async () => {
594
+ try {
595
+ // First, resolve the version from Rivendell
596
+ const resolveResponse = await fetch("/devserver/api/deploy/resolve-version", {
597
+ method: "POST",
598
+ headers: { "Content-Type": "application/json" },
599
+ });
600
+ if (!resolveResponse.ok) {
601
+ const errorData = await resolveResponse.json().catch(() => ({}));
602
+ appendDeployLog(`Error resolving version: ${errorData.error || 'Unknown error'}`);
603
+ return;
604
+ }
605
+ const versionData = await resolveResponse.json();
606
+ if (versionData.searchError) {
607
+ appendDeployLog(`Warning: Rivendell search failed (${versionData.searchError}), bumping version locally`);
608
+ }
609
+ if (versionData.isDevVersion && (0, DeployModal_1.shouldSkipDevConfirmation)()) {
610
+ // Dev version with "do not show again" enabled: deploy immediately
611
+ startDeploy(versionData.suggestedVersion);
612
+ }
613
+ else {
614
+ // Show confirmation modal
615
+ setDeployModalData({
616
+ currentVersion: versionData.currentVersion,
617
+ suggestedVersion: versionData.suggestedVersion,
618
+ isDevVersion: versionData.isDevVersion
619
+ });
620
+ setShowDeployModal(true);
621
+ }
622
+ }
623
+ catch (error) {
624
+ console.error("Error triggering deploy:", error);
625
+ appendDeployLog(`Error: ${error instanceof Error ? error.message : String(error)}`);
626
+ }
627
+ };
628
+ // Start the deploy with a specific version
629
+ const startDeploy = async (version) => {
630
+ setShowDeployModal(false);
631
+ try {
632
+ const response = await fetch("/devserver/api/deploy/start", {
633
+ method: "POST",
634
+ headers: { "Content-Type": "application/json" },
635
+ body: JSON.stringify({ version }),
636
+ });
637
+ if (!response.ok) {
638
+ const errorData = await response.json().catch(() => ({}));
639
+ appendDeployLog(`Error starting deploy: ${errorData.error || 'Unknown error'}`);
640
+ }
641
+ // Success: SSE events will handle state updates
642
+ }
643
+ catch (error) {
644
+ console.error("Error starting deploy:", error);
645
+ appendDeployLog(`Error: ${error instanceof Error ? error.message : String(error)}`);
646
+ }
647
+ };
648
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "app postman-layout", children: [(0, jsx_runtime_1.jsxs)("header", { className: "top-nav", children: [(0, jsx_runtime_1.jsx)("div", { className: "nav-left", children: (0, jsx_runtime_1.jsx)("div", { className: "app-title", children: (0, jsx_runtime_1.jsx)("h1", { children: "OCP Local Environment" }) }) }), appInfo && ((0, jsx_runtime_1.jsx)("div", { className: "nav-center", children: (0, jsx_runtime_1.jsxs)("div", { className: "app-info-nav", children: [(0, jsx_runtime_1.jsx)("span", { className: "app-name", children: appInfo.displayName || appInfo.name }), (0, jsx_runtime_1.jsxs)("span", { className: "app-version", children: ["v", appInfo.version] })] }) })), (0, jsx_runtime_1.jsxs)("div", { className: "nav-right", children: [(0, jsx_runtime_1.jsxs)("button", { className: `deploy-btn ${getDeployStatusClass()}`, onClick: triggerDeploy, disabled: deployStatus.deployInProgress || !!buildStatus?.buildInProgress, title: "Deploy app version to OCP", children: [(0, jsx_runtime_1.jsx)("span", { className: "deploy-status-dot" }), deployStatus.deployInProgress ? "Deploying..." :
649
+ deployStatus.lastDeploySuccess === true ? "Deployed" : "Deploy"] }), (0, jsx_runtime_1.jsxs)("button", { className: `rebuild-btn ${buildStatus?.buildInProgress ? 'building' :
408
650
  buildStatus?.lastBuildSuccess === true ? 'success' :
409
- buildStatus?.lastBuildSuccess === false ? 'error' : ''}`, onClick: triggerBuild, disabled: buildStatus?.buildInProgress, children: [(0, jsx_runtime_1.jsx)("span", { className: "rebuild-status-dot" }), buildStatus?.buildInProgress ? "Building..." : "Rebuild"] }), (0, jsx_runtime_1.jsx)("div", { className: "nav-separator-vertical" }), (0, jsx_runtime_1.jsx)("a", { href: "https://github.com/optimizely/ocp-developer-feedback/issues/new/choose", target: "_blank", rel: "noopener noreferrer", className: "feedback-btn", title: "Provide Feedback", children: "\uD83D\uDCAC Feedback" }), (0, jsx_runtime_1.jsxs)("a", { href: "https://docs.developers.optimizely.com/optimizely-connect-platform/docs/local-testing-tool-ocp2", target: "_blank", rel: "noopener noreferrer", className: "docs-btn", title: "Documentation", children: [(0, jsx_runtime_1.jsx)("span", { className: "docs-icon", children: "\uD83D\uDCD6" }), " Docs"] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "main-layout", children: [(0, jsx_runtime_1.jsx)("aside", { className: "sidebar", children: (0, jsx_runtime_1.jsxs)("nav", { className: "sidebar-nav", children: [(0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "settings" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/settings'), children: [(0, jsx_runtime_1.jsx)("span", { className: "nav-icon", children: "\u2699\uFE0F" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "App Settings" })] }), appInfo && appInfo.functions && appInfo.functions.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "functions" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/functions'), children: [(0, jsx_runtime_1.jsx)("span", { className: "nav-icon", children: "\u26A1" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Functions" })] })), appInfo && appInfo.jobs && appInfo.jobs.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "jobs" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/jobs'), children: [(0, jsx_runtime_1.jsx)("span", { className: "nav-icon", children: "\uD83D\uDCCB" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Jobs" })] })), appInfo && appInfo.destinations && appInfo.destinations.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "destinations" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/destinations'), children: [(0, jsx_runtime_1.jsx)("span", { className: "nav-icon", children: "\uD83D\uDCE4" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Destinations" })] })), appInfo && appInfo.sources && appInfo.sources.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "sources" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/sources'), children: [(0, jsx_runtime_1.jsx)("span", { className: "nav-icon", children: "\uD83D\uDCE5" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Sources" })] }))] }) }), (0, jsx_runtime_1.jsxs)("main", { className: "content-area", children: [(0, jsx_runtime_1.jsxs)("div", { className: "content-header", children: [(0, jsx_runtime_1.jsxs)("h2", { children: [activeView === "settings" && "App Settings", activeView === "functions" && "Functions", activeView === "jobs" && "Jobs", activeView === "destinations" && "Destinations", activeView === "sources" && "Sources"] }), appInfo && !appInfo.isValid && ((0, jsx_runtime_1.jsxs)("div", { className: "validation-errors-banner", children: [(0, jsx_runtime_1.jsx)("span", { className: "error-icon", children: "\u26A0\uFE0F" }), (0, jsx_runtime_1.jsx)("span", { children: "App has validation errors" })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "content-body", children: (0, jsx_runtime_1.jsxs)(react_router_dom_1.Routes, { children: [(0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/", element: (0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true }) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/settings", element: (0, jsx_runtime_1.jsxs)("div", { className: "settings-view", children: [appInfo && !appInfo.isValid && appInfo.errors && appInfo.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "validation-errors", children: [(0, jsx_runtime_1.jsx)("h4", { children: "Validation Errors:" }), (0, jsx_runtime_1.jsx)("ul", { children: appInfo.errors.map((err, idx) => ((0, jsx_runtime_1.jsx)("li", { children: err }, idx))) })] })), buildStatus?.buildInProgress || (buildStatus?.lastBuildSuccess === null && buildStatus?.lastBuildTimestamp === null) ? ((0, jsx_runtime_1.jsx)("div", { className: "settings-loading-container", children: (0, jsx_runtime_1.jsxs)("div", { className: "placeholder-content", children: [(0, jsx_runtime_1.jsx)("h3", { children: "Building App..." }), (0, jsx_runtime_1.jsx)("p", { children: "Please wait while the app is being built." }), (0, jsx_runtime_1.jsx)("div", { className: "spinner" })] }) })) : buildStatus?.lastBuildSuccess === false ? ((0, jsx_runtime_1.jsx)("div", { className: "settings-loading-container", children: (0, jsx_runtime_1.jsxs)("div", { className: "placeholder-content", children: [(0, jsx_runtime_1.jsx)("h3", { children: "Build Failed" }), (0, jsx_runtime_1.jsx)("p", { children: "The app failed to build. Please check the console for errors." }), buildStatus.lastBuildError && ((0, jsx_runtime_1.jsx)("pre", { className: "error-details", children: buildStatus.lastBuildError }))] }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "settings-iframe-container", children: (0, jsx_runtime_1.jsx)("iframe", { id: "app_directory_frame", style: {
651
+ buildStatus?.lastBuildSuccess === false ? 'error' : ''}`, onClick: triggerBuild, disabled: buildStatus?.buildInProgress, children: [(0, jsx_runtime_1.jsx)("span", { className: "rebuild-status-dot" }), buildStatus?.buildInProgress ? "Building..." : "Rebuild"] }), config.hasSchemaFiles && ((0, jsx_runtime_1.jsxs)("div", { className: "odp-status-badge", children: [(0, jsx_runtime_1.jsx)("span", { className: `odp-status-dot ${config.odpKeyConfigured ? 'configured' : 'not-configured'}` }), "ODP/OCP", (0, jsx_runtime_1.jsx)("span", { className: "odp-tooltip", children: config.odpKeyConfigured
652
+ ? 'ODP API Key is configured'
653
+ : 'ODP API Key is not configured — set it in Tool Settings' })] })), (0, jsx_runtime_1.jsx)("div", { className: "nav-separator-vertical" }), (0, jsx_runtime_1.jsxs)("a", { href: "https://github.com/optimizely/ocp-developer-feedback/issues/new/choose", target: "_blank", rel: "noopener noreferrer", className: "feedback-btn", title: "Provide Feedback", children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconPenField, { size: "14" }), "Feedback"] }), (0, jsx_runtime_1.jsxs)("a", { href: "https://docs.developers.optimizely.com/optimizely-connect-platform/docs/local-testing", target: "_blank", rel: "noopener noreferrer", className: "docs-btn", title: "Documentation", children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconBookOpen, { size: "14" }), " Docs"] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "main-layout", children: [(0, jsx_runtime_1.jsx)("aside", { className: "sidebar", children: (0, jsx_runtime_1.jsxs)("nav", { className: "sidebar-nav", children: [(0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "settings" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/settings'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconGear, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "App Settings" })] }), appInfo && appInfo.functions && appInfo.functions.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "functions" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/functions'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconBolt, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Functions" })] })), appInfo && appInfo.jobs && appInfo.jobs.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "jobs" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/jobs'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconCubes, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Jobs" })] })), appInfo && appInfo.destinations && appInfo.destinations.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "destinations" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/destinations'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconUpload, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Destinations" })] })), appInfo && appInfo.sources && appInfo.sources.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "sources" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/sources'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconDownload, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Sources" })] })), appInfo && appInfo.opalTools && appInfo.opalTools.length > 0 && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "opal-tools" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/opal-tools'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconScrewdriverWrench, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Opal Tools" })] })), config.hasSchemaFiles && ((0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "odp-schema" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/odp-schema'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconMemoPad, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "ODP Schema" })] })), (0, jsx_runtime_1.jsx)("div", { className: "nav-separator", style: { height: '1px', background: '#3e3e42', margin: '4px 0' } }), (0, jsx_runtime_1.jsxs)("button", { className: `nav-item ${activeView === "tool-settings" ? "active" : ""}`, onClick: () => navigatePreservingConsole('/tool-settings'), children: [(0, jsx_runtime_1.jsx)(axiom_icons_1.IconGear, { size: "2xs" }), (0, jsx_runtime_1.jsx)("span", { className: "nav-label", children: "Tool Settings" })] })] }) }), (0, jsx_runtime_1.jsxs)("main", { className: "content-area", children: [activeView !== "jobs" && activeView !== "functions" && activeView !== "opal-tools" && activeView !== "destinations" && activeView !== "tool-settings" && activeView !== "odp-schema" && ((0, jsx_runtime_1.jsxs)("div", { className: "content-header", children: [(0, jsx_runtime_1.jsxs)("h2", { children: [activeView === "settings" && "App Settings", activeView === "sources" && "Sources"] }), appInfo && !appInfo.isValid && ((0, jsx_runtime_1.jsxs)("div", { className: "validation-errors-banner", children: [(0, jsx_runtime_1.jsx)("span", { className: "error-icon", children: "\u26A0\uFE0F" }), (0, jsx_runtime_1.jsx)("span", { children: "App has validation errors" })] }))] })), (0, jsx_runtime_1.jsx)("div", { className: "content-body", children: (0, jsx_runtime_1.jsxs)(react_router_dom_1.Routes, { children: [(0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/", element: (0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true }) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/settings", element: (0, jsx_runtime_1.jsxs)("div", { className: "settings-view", children: [appInfo && !appInfo.isValid && appInfo.errors && appInfo.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "validation-errors", children: [(0, jsx_runtime_1.jsx)("h4", { children: "Validation Errors:" }), (0, jsx_runtime_1.jsx)("ul", { children: appInfo.errors.map((err, idx) => ((0, jsx_runtime_1.jsx)("li", { children: err }, idx))) })] })), buildStatus?.buildInProgress || (buildStatus?.lastBuildSuccess === null && buildStatus?.lastBuildTimestamp === null) ? ((0, jsx_runtime_1.jsx)("div", { className: "settings-loading-container", children: (0, jsx_runtime_1.jsxs)("div", { className: "placeholder-content", children: [(0, jsx_runtime_1.jsx)("h3", { children: "Building App..." }), (0, jsx_runtime_1.jsx)("p", { children: "Please wait while the app is being built." }), (0, jsx_runtime_1.jsx)("div", { className: "spinner" })] }) })) : buildStatus?.lastBuildSuccess === false ? ((0, jsx_runtime_1.jsx)("div", { className: "settings-loading-container", children: (0, jsx_runtime_1.jsxs)("div", { className: "placeholder-content", children: [(0, jsx_runtime_1.jsx)("h3", { children: "Build Failed" }), (0, jsx_runtime_1.jsx)("p", { children: "The app failed to build. Please check the console for errors." }), buildStatus.lastBuildError && ((0, jsx_runtime_1.jsx)("pre", { className: "error-details", children: buildStatus.lastBuildError }))] }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "settings-iframe-container", children: (0, jsx_runtime_1.jsx)("iframe", { id: "app_directory_frame", style: {
410
654
  width: "100%",
411
655
  height: "100%",
412
656
  border: "none",
413
657
  overflow: "hidden",
414
- }, ref: setIframeRef, src: `${window.location.origin}/app/${appInfo?.name || "unknown"}`, onLoad: handleIframeLoad }) }))] }) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/functions/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "functions-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.functions && appInfo.functions.length > 0 ? ((0, jsx_runtime_1.jsx)(FunctionsView_1.default, { appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/jobs/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "jobs-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.jobs && appInfo.jobs.length > 0 ? ((0, jsx_runtime_1.jsx)(JobsView_1.default, { appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/destinations/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "destinations-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.destinations && appInfo.destinations.length > 0 ? ((0, jsx_runtime_1.jsx)(DestinationsView_1.DestinationsView, { apiBaseUrl: "/devserver/api", appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/sources/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "sources-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.sources && appInfo.sources.length > 0 ? ((0, jsx_runtime_1.jsx)(SourcesView_1.SourcesView, { apiBaseUrl: "/devserver/api", appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "*", element: (0, jsx_runtime_1.jsxs)("div", { className: "not-found-view", style: {
658
+ }, ref: setIframeRef, src: `${window.location.origin}/app/${appInfo?.name || "unknown"}`, onLoad: handleIframeLoad }) }))] }) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/functions/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "functions-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.functions && appInfo.functions.length > 0 ? ((0, jsx_runtime_1.jsx)(FunctionsView_1.default, { appInstalled: appInstalled, buildStatus: buildStatus, installVersion: installVersion })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/jobs/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "jobs-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.jobs && appInfo.jobs.length > 0 ? ((0, jsx_runtime_1.jsx)(JobsView_1.default, { appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/destinations/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "destinations-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.destinations && appInfo.destinations.length > 0 ? ((0, jsx_runtime_1.jsx)(DestinationsView_1.DestinationsView, { apiBaseUrl: "/devserver/api", appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/sources/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "sources-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.sources && appInfo.sources.length > 0 ? ((0, jsx_runtime_1.jsx)(SourcesView_1.SourcesView, { apiBaseUrl: "/devserver/api", appInstalled: appInstalled, buildStatus: buildStatus })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/opal-tools/*", element: !appInfo ? ((0, jsx_runtime_1.jsx)("div", { className: "functions-view", children: (0, jsx_runtime_1.jsx)("div", { className: "placeholder-content", children: (0, jsx_runtime_1.jsx)("p", { children: "Loading..." }) }) })) : appInfo.opalTools && appInfo.opalTools.length > 0 ? ((0, jsx_runtime_1.jsx)(OpalToolsView_1.default, { appInstalled: appInstalled, buildStatus: buildStatus, installVersion: installVersion })) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/odp-schema", element: config.hasSchemaFiles ? ((0, jsx_runtime_1.jsx)(OdpSchemaView_1.default, {})) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/settings", replace: true })) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "/tool-settings", element: (0, jsx_runtime_1.jsx)(ToolSettingsView_1.default, { odpApiKey: config.odpApiKey || '', onSave: async (odpApiKey) => {
659
+ try {
660
+ await fetch('/devserver/api/config/odp', {
661
+ method: 'POST',
662
+ headers: { 'Content-Type': 'application/json' },
663
+ body: JSON.stringify({ odpApiKey }),
664
+ });
665
+ fetchConfig();
666
+ }
667
+ catch (error) {
668
+ console.error('Error saving ODP config:', error);
669
+ }
670
+ } }) }), (0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: "*", element: (0, jsx_runtime_1.jsxs)("div", { className: "not-found-view", style: {
415
671
  display: 'flex',
416
672
  flexDirection: 'column',
417
673
  alignItems: 'center',
@@ -426,7 +682,7 @@ const App = () => {
426
682
  color: 'white',
427
683
  border: 'none',
428
684
  borderRadius: '4px'
429
- }, children: "Go to Settings" })] }) })] }) })] })] }), (0, jsx_runtime_1.jsx)(TabbedConsole_1.default, { logs: logs, onClearLogs: clearConsoleLogs, storeTimestamps: storeTimestamps, onRefreshStores: fetchStatus, notificationCount: notificationCount })] }));
685
+ }, children: "Go to Settings" })] }) })] }) })] })] }), (0, jsx_runtime_1.jsx)(TabbedConsole_1.default, { logs: logs, onClearLogs: clearConsoleLogs, storeTimestamps: storeTimestamps, onRefreshStores: fetchStatus, notificationCount: notificationCount, hasSources: !!(appInfo?.sources && appInfo.sources.length > 0), sourceDataVersion: sourceDataVersion, sourceDataTotalEmitted: sourceDataTotalEmitted, notificationVersion: notificationVersion, envVersion: envVersion, deployLogs: deployLogs, deployVersion: deployVersion, deployStatus: deployStatus }), (0, jsx_runtime_1.jsx)(DeployModal_1.DeployModal, { isOpen: showDeployModal, onClose: () => setShowDeployModal(false), currentVersion: deployModalData?.currentVersion || '', suggestedVersion: deployModalData?.suggestedVersion || '', isDevVersion: deployModalData?.isDevVersion || false, onDeploy: startDeploy }), permanentlyDisconnected && ((0, jsx_runtime_1.jsx)("div", { className: "disconnect-overlay", children: (0, jsx_runtime_1.jsxs)("div", { className: "disconnect-message", children: [(0, jsx_runtime_1.jsx)("h2", { children: "Server Disconnected" }), (0, jsx_runtime_1.jsx)("p", { children: "The development server has been stopped or is unreachable." }), (0, jsx_runtime_1.jsx)("button", { onClick: () => window.location.reload(), children: "Reconnect" })] }) })), appChanged && ((0, jsx_runtime_1.jsx)("div", { className: "disconnect-overlay", children: (0, jsx_runtime_1.jsxs)("div", { className: "disconnect-message app-changed", children: [(0, jsx_runtime_1.jsx)("h2", { children: "Different App Detected" }), (0, jsx_runtime_1.jsxs)("p", { children: ["The server is now running a different app.", (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("span", { className: "app-change-details", children: [(0, jsx_runtime_1.jsx)("strong", { children: appChanged.oldApp }), " \u2192 ", (0, jsx_runtime_1.jsx)("strong", { children: appChanged.newApp })] })] }), (0, jsx_runtime_1.jsx)("button", { onClick: () => window.location.reload(), children: "Load New App" })] }) }))] }));
430
686
  };
431
687
  exports.default = App;
432
688
  //# sourceMappingURL=App.js.map