Stijnus commited on
Commit
9171cf4
·
1 Parent(s): 8035a76

bug fix and some icons changes

Browse files
app/components/@settings/core/ControlPanel.tsx CHANGED
@@ -11,11 +11,15 @@ import { useFeatures } from '~/lib/hooks/useFeatures';
11
  import { useNotifications } from '~/lib/hooks/useNotifications';
12
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
13
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
14
- import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
 
 
 
 
 
15
  import { profileStore } from '~/lib/stores/profile';
16
- import type { TabType, TabVisibilityConfig, DevTabConfig, Profile } from './types';
17
  import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
18
- import { resetTabConfiguration } from '~/lib/stores/settings';
19
  import { DialogTitle } from '~/components/ui/Dialog';
20
  import { AvatarDropdown } from './AvatarDropdown';
21
 
@@ -43,6 +47,24 @@ interface TabWithDevType extends TabVisibilityConfig {
43
  isExtraDevTab?: boolean;
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  const TAB_DESCRIPTIONS: Record<TabType, string> = {
47
  profile: 'Manage your profile and account settings',
48
  settings: 'Configure application preferences',
@@ -60,6 +82,65 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
60
  'tab-management': 'Configure visible tabs and their order',
61
  };
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
64
  // State
65
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
@@ -78,7 +159,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
78
  const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
79
  const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
80
 
81
- // Add visibleTabs logic using useMemo
 
 
 
 
 
82
  const visibleTabs = useMemo(() => {
83
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
84
  console.warn('Invalid tab configuration, resetting to defaults');
@@ -87,64 +173,84 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
87
  return [];
88
  }
89
 
 
 
90
  // In developer mode, show ALL tabs without restrictions
91
  if (developerMode) {
92
- // Combine all unique tabs from both user and developer configurations
93
- const allTabs = new Set([
94
- ...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
95
- ...tabConfiguration.userTabs.map((tab) => tab.id),
96
- ...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
97
- ]);
98
-
99
- // Create a complete tab list with all tabs visible
100
- const devTabs = Array.from(allTabs).map((tabId) => {
101
- // Try to find existing configuration for this tab
102
- const existingTab =
103
- tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
104
- tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
105
- DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
106
-
107
- return {
108
- id: tabId,
109
- visible: true,
110
- window: 'developer' as const,
111
- order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
112
- };
113
- });
114
 
115
- // Add Tab Management tile for developer mode
116
- const tabManagementConfig: DevTabConfig = {
117
- id: 'tab-management',
118
  visible: true,
119
  window: 'developer',
120
  order: devTabs.length,
121
  isExtraDevTab: true,
122
- };
123
- devTabs.push(tabManagementConfig);
124
 
125
  return devTabs.sort((a, b) => a.order - b.order);
126
  }
127
 
128
- // In user mode, only show visible user tabs
129
- const notificationsDisabled = profile?.preferences?.notifications === false;
130
-
131
  return tabConfiguration.userTabs
132
  .filter((tab) => {
133
- if (!tab || typeof tab.id !== 'string') {
134
- console.warn('Invalid tab entry:', tab);
135
  return false;
136
  }
137
 
138
- // Hide notifications tab if notifications are disabled in user preferences
139
  if (tab.id === 'notifications' && notificationsDisabled) {
140
  return false;
141
  }
142
 
143
- // Only show tabs that are explicitly visible and assigned to the user window
144
  return tab.visible && tab.window === 'user';
145
  })
146
  .sort((a, b) => a.order - b.order);
147
- }, [tabConfiguration, developerMode, profile?.preferences?.notifications]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  // Handlers
150
  const handleBack = () => {
@@ -328,7 +434,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
328
  }}
329
  >
330
  <div className="w-full h-full flex items-center justify-center bg-gray-100/50 dark:bg-gray-800/50 rounded-full">
331
- <div className="i-ph:robot-fill w-5 h-5 text-gray-400 dark:text-gray-400 transition-colors" />
332
  </div>
333
  </motion.div>
334
  )}
@@ -338,39 +444,14 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
338
  </div>
339
 
340
  <div className="flex items-center gap-6">
341
- {/* Developer Mode Controls */}
342
- <div className="flex items-center gap-6">
343
- {/* Mode Toggle */}
344
- <div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
345
- <Switch
346
- id="developer-mode"
347
- checked={developerMode}
348
- onCheckedChange={handleDeveloperModeChange}
349
- className={classNames(
350
- 'relative inline-flex h-6 w-11 items-center rounded-full',
351
- 'bg-gray-200 dark:bg-gray-700',
352
- 'data-[state=checked]:bg-purple-500',
353
- 'transition-colors duration-200',
354
- )}
355
- >
356
- <span className="sr-only">Toggle developer mode</span>
357
- <span
358
- className={classNames(
359
- 'inline-block h-4 w-4 transform rounded-full bg-white',
360
- 'transition duration-200',
361
- 'translate-x-1 data-[state=checked]:translate-x-6',
362
- )}
363
- />
364
- </Switch>
365
- <div className="flex items-center gap-2">
366
- <label
367
- htmlFor="developer-mode"
368
- className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
369
- >
370
- {developerMode ? 'Developer Mode' : 'User Mode'}
371
- </label>
372
- </div>
373
- </div>
374
  </div>
375
 
376
  {/* Avatar and Dropdown */}
@@ -415,24 +496,15 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
415
  ) : activeTab ? (
416
  getTabComponent(activeTab)
417
  ) : (
418
- <motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
 
 
 
 
 
419
  <AnimatePresence mode="popLayout">
420
  {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
421
- <motion.div
422
- key={tab.id}
423
- layout
424
- initial={{ opacity: 0, scale: 0.8 }}
425
- animate={{ opacity: 1, scale: 1 }}
426
- exit={{ opacity: 0, scale: 0.8 }}
427
- transition={{
428
- type: 'spring',
429
- stiffness: 400,
430
- damping: 30,
431
- mass: 0.8,
432
- duration: 0.3,
433
- }}
434
- className="aspect-[1.5/1]"
435
- >
436
  <TabTile
437
  tab={tab}
438
  onClick={() => handleTabClick(tab.id as TabType)}
 
11
  import { useNotifications } from '~/lib/hooks/useNotifications';
12
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
13
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
14
+ import {
15
+ tabConfigurationStore,
16
+ developerModeStore,
17
+ setDeveloperMode,
18
+ resetTabConfiguration,
19
+ } from '~/lib/stores/settings';
20
  import { profileStore } from '~/lib/stores/profile';
21
+ import type { TabType, TabVisibilityConfig, Profile } from './types';
22
  import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
 
23
  import { DialogTitle } from '~/components/ui/Dialog';
24
  import { AvatarDropdown } from './AvatarDropdown';
25
 
 
47
  isExtraDevTab?: boolean;
48
  }
49
 
50
+ interface ExtendedTabConfig extends TabVisibilityConfig {
51
+ isExtraDevTab?: boolean;
52
+ }
53
+
54
+ interface BaseTabConfig {
55
+ id: TabType;
56
+ visible: boolean;
57
+ window: 'user' | 'developer';
58
+ order: number;
59
+ }
60
+
61
+ interface AnimatedSwitchProps {
62
+ checked: boolean;
63
+ onCheckedChange: (checked: boolean) => void;
64
+ id: string;
65
+ label: string;
66
+ }
67
+
68
  const TAB_DESCRIPTIONS: Record<TabType, string> = {
69
  profile: 'Manage your profile and account settings',
70
  settings: 'Configure application preferences',
 
82
  'tab-management': 'Configure visible tabs and their order',
83
  };
84
 
85
+ const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => {
86
+ return (
87
+ <div className="flex items-center gap-2">
88
+ <Switch
89
+ id={id}
90
+ checked={checked}
91
+ onCheckedChange={onCheckedChange}
92
+ className={classNames(
93
+ 'relative inline-flex h-6 w-11 items-center rounded-full',
94
+ 'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]',
95
+ 'bg-gray-200 dark:bg-gray-700',
96
+ 'data-[state=checked]:bg-purple-500',
97
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
98
+ 'cursor-pointer',
99
+ 'group',
100
+ )}
101
+ >
102
+ <motion.span
103
+ className={classNames(
104
+ 'absolute left-[2px] top-[2px]',
105
+ 'inline-block h-5 w-5 rounded-full',
106
+ 'bg-white shadow-lg',
107
+ 'transition-shadow duration-300',
108
+ 'group-hover:shadow-md group-active:shadow-sm',
109
+ 'group-hover:scale-95 group-active:scale-90',
110
+ )}
111
+ layout
112
+ transition={{
113
+ type: 'spring',
114
+ stiffness: 500,
115
+ damping: 30,
116
+ }}
117
+ animate={{
118
+ x: checked ? '1.25rem' : '0rem',
119
+ }}
120
+ >
121
+ <motion.div
122
+ className="absolute inset-0 rounded-full bg-white"
123
+ initial={false}
124
+ animate={{
125
+ scale: checked ? 1 : 0.8,
126
+ }}
127
+ transition={{ duration: 0.2 }}
128
+ />
129
+ </motion.span>
130
+ <span className="sr-only">Toggle {label}</span>
131
+ </Switch>
132
+ <div className="flex items-center gap-2">
133
+ <label
134
+ htmlFor={id}
135
+ className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
136
+ >
137
+ {label}
138
+ </label>
139
+ </div>
140
+ </div>
141
+ );
142
+ };
143
+
144
  export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
145
  // State
146
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
 
159
  const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
160
  const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
161
 
162
+ // Memoize the base tab configurations to avoid recalculation
163
+ const baseTabConfig = useMemo(() => {
164
+ return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
165
+ }, []);
166
+
167
+ // Add visibleTabs logic using useMemo with optimized calculations
168
  const visibleTabs = useMemo(() => {
169
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
170
  console.warn('Invalid tab configuration, resetting to defaults');
 
173
  return [];
174
  }
175
 
176
+ const notificationsDisabled = profile?.preferences?.notifications === false;
177
+
178
  // In developer mode, show ALL tabs without restrictions
179
  if (developerMode) {
180
+ const seenTabs = new Set<TabType>();
181
+ const devTabs: ExtendedTabConfig[] = [];
182
+
183
+ // Process tabs in order of priority: developer, user, default
184
+ const processTab = (tab: BaseTabConfig) => {
185
+ if (!seenTabs.has(tab.id)) {
186
+ seenTabs.add(tab.id);
187
+ devTabs.push({
188
+ id: tab.id,
189
+ visible: true,
190
+ window: 'developer',
191
+ order: tab.order || devTabs.length,
192
+ });
193
+ }
194
+ };
195
+
196
+ // Process tabs in priority order
197
+ tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig));
198
+ tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig));
199
+ DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
 
 
200
 
201
+ // Add Tab Management tile
202
+ devTabs.push({
203
+ id: 'tab-management' as TabType,
204
  visible: true,
205
  window: 'developer',
206
  order: devTabs.length,
207
  isExtraDevTab: true,
208
+ });
 
209
 
210
  return devTabs.sort((a, b) => a.order - b.order);
211
  }
212
 
213
+ // Optimize user mode tab filtering
 
 
214
  return tabConfiguration.userTabs
215
  .filter((tab) => {
216
+ if (!tab?.id) {
 
217
  return false;
218
  }
219
 
 
220
  if (tab.id === 'notifications' && notificationsDisabled) {
221
  return false;
222
  }
223
 
 
224
  return tab.visible && tab.window === 'user';
225
  })
226
  .sort((a, b) => a.order - b.order);
227
+ }, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
228
+
229
+ // Optimize animation performance with layout animations
230
+ const gridLayoutVariants = {
231
+ hidden: { opacity: 0 },
232
+ visible: {
233
+ opacity: 1,
234
+ transition: {
235
+ staggerChildren: 0.05,
236
+ delayChildren: 0.1,
237
+ },
238
+ },
239
+ };
240
+
241
+ const itemVariants = {
242
+ hidden: { opacity: 0, scale: 0.8 },
243
+ visible: {
244
+ opacity: 1,
245
+ scale: 1,
246
+ transition: {
247
+ type: 'spring',
248
+ stiffness: 200,
249
+ damping: 20,
250
+ mass: 0.6,
251
+ },
252
+ },
253
+ };
254
 
255
  // Handlers
256
  const handleBack = () => {
 
434
  }}
435
  >
436
  <div className="w-full h-full flex items-center justify-center bg-gray-100/50 dark:bg-gray-800/50 rounded-full">
437
+ <div className="i-ph:lightning-fill w-5 h-5 text-purple-500 dark:text-purple-400 transition-colors" />
438
  </div>
439
  </motion.div>
440
  )}
 
444
  </div>
445
 
446
  <div className="flex items-center gap-6">
447
+ {/* Mode Toggle */}
448
+ <div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
449
+ <AnimatedSwitch
450
+ id="developer-mode"
451
+ checked={developerMode}
452
+ onCheckedChange={handleDeveloperModeChange}
453
+ label={developerMode ? 'Developer Mode' : 'User Mode'}
454
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  </div>
456
 
457
  {/* Avatar and Dropdown */}
 
496
  ) : activeTab ? (
497
  getTabComponent(activeTab)
498
  ) : (
499
+ <motion.div
500
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
501
+ variants={gridLayoutVariants}
502
+ initial="hidden"
503
+ animate="visible"
504
+ >
505
  <AnimatePresence mode="popLayout">
506
  {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
507
+ <motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  <TabTile
509
  tab={tab}
510
  onClick={() => handleTabClick(tab.id as TabType)}
app/components/@settings/tabs/update/UpdateTab.tsx CHANGED
@@ -22,6 +22,19 @@ interface GitHubReleaseResponse {
22
  }>;
23
  }
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  interface UpdateInfo {
26
  currentVersion: string;
27
  latestVersion: string;
@@ -32,9 +45,7 @@ interface UpdateInfo {
32
  changelog?: string[];
33
  currentCommit?: string;
34
  latestCommit?: string;
35
- downloadProgress?: number;
36
- installProgress?: number;
37
- estimatedTimeRemaining?: number;
38
  error?: {
39
  type: string;
40
  message: string;
@@ -47,13 +58,6 @@ interface UpdateSettings {
47
  checkInterval: number;
48
  }
49
 
50
- interface UpdateResponse {
51
- success: boolean;
52
- error?: string;
53
- message?: string;
54
- instructions?: string[];
55
- }
56
-
57
  const categorizeChangelog = (messages: string[]) => {
58
  const categories = new Map<string, string[]>();
59
 
@@ -168,7 +172,6 @@ const UpdateTab = () => {
168
  const [isChecking, setIsChecking] = useState(false);
169
  const [isUpdating, setIsUpdating] = useState(false);
170
  const [error, setError] = useState<string | null>(null);
171
- const [retryCount, setRetryCount] = useState(0);
172
  const [showChangelog, setShowChangelog] = useState(false);
173
  const [showManualInstructions, setShowManualInstructions] = useState(false);
174
  const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
@@ -186,6 +189,7 @@ const UpdateTab = () => {
186
  const [lastChecked, setLastChecked] = useState<Date | null>(null);
187
  const [showUpdateDialog, setShowUpdateDialog] = useState(false);
188
  const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
 
189
 
190
  useEffect(() => {
191
  localStorage.setItem('update_settings', JSON.stringify(updateSettings));
@@ -259,78 +263,105 @@ const UpdateTab = () => {
259
  const initiateUpdate = async () => {
260
  setIsUpdating(true);
261
  setError(null);
 
262
 
263
- let currentRetry = 0;
264
- const maxRetries = 3;
265
-
266
- const attemptUpdate = async (): Promise<void> => {
267
- try {
268
- const response = await fetch('/api/update', {
269
- method: 'POST',
270
- headers: {
271
- 'Content-Type': 'application/json',
272
- },
273
- body: JSON.stringify({
274
- branch: isLatestBranch ? 'main' : 'stable',
275
- }),
276
- });
 
277
 
278
- if (!response.ok) {
279
- const errorData = (await response.json()) as { error: string };
280
- throw new Error(errorData.error || 'Failed to initiate update');
281
- }
282
 
283
- const result = (await response.json()) as UpdateResponse;
 
 
284
 
285
- if (result.success) {
286
- logStore.logSuccess('Update instructions ready', {
287
- type: 'update',
288
- message: result.message || 'Update instructions ready',
289
- });
290
 
291
- // Show manual update instructions
292
- setShowManualInstructions(true);
293
- setUpdateChangelog(
294
- result.instructions || [
295
- 'Failed to get update instructions. Please update manually:',
296
- '1. git pull origin main',
297
- '2. pnpm install',
298
- '3. pnpm build',
299
- '4. Restart the application',
300
- ],
301
- );
302
-
303
- return;
304
  }
305
 
306
- throw new Error(result.error || 'Update failed');
307
- } catch (err) {
308
- currentRetry++;
309
 
310
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
 
 
 
311
 
312
- if (currentRetry < maxRetries) {
313
- toast.warning(`Update attempt ${currentRetry} failed. Retrying...`, { autoClose: 2000 });
314
- setRetryCount(currentRetry);
315
- await new Promise((resolve) => setTimeout(resolve, 2000));
316
- await attemptUpdate();
317
 
318
- return;
319
- }
 
 
 
 
 
320
 
321
- setError('Failed to get update instructions. Please update manually.');
322
- console.error('Update failed:', err);
323
- logStore.logSystem('Update failed: ' + errorMessage);
324
- toast.error('Update failed: ' + errorMessage);
325
- setUpdateFailed(true);
 
 
 
 
 
 
326
  }
327
- };
 
 
 
 
 
 
 
 
 
 
328
 
329
- await attemptUpdate();
330
- setIsUpdating(false);
331
- setRetryCount(0);
 
 
 
 
 
 
332
  };
333
 
 
 
 
 
 
 
 
 
 
 
334
  useEffect(() => {
335
  const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
336
  const intervalId = setInterval(checkForUpdates, checkInterval);
@@ -741,7 +772,7 @@ const UpdateTab = () => {
741
  )}
742
 
743
  {/* Update Progress */}
744
- {isUpdating && updateInfo?.downloadProgress !== undefined && (
745
  <motion.div
746
  className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
747
  initial={{ opacity: 0, y: 20 }}
@@ -750,18 +781,83 @@ const UpdateTab = () => {
750
  >
751
  <div className="space-y-4">
752
  <div className="flex items-center justify-between">
753
- <span className="text-sm text-bolt-elements-textPrimary">Downloading Update</span>
754
- <span className="text-sm text-bolt-elements-textSecondary">
755
- {Math.round(updateInfo.downloadProgress)}%
756
- </span>
757
- </div>
758
- <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
759
- <div
760
- className="h-full bg-purple-500 transition-all duration-300"
761
- style={{ width: `${updateInfo.downloadProgress}%` }}
762
- />
763
  </div>
764
- {retryCount > 0 && <p className="text-sm text-yellow-500">Retry attempt {retryCount}/3...</p>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  </div>
766
  </motion.div>
767
  )}
 
22
  }>;
23
  }
24
 
25
+ interface UpdateProgress {
26
+ stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
27
+ message: string;
28
+ progress?: number;
29
+ error?: string;
30
+ details?: {
31
+ changedFiles?: string[];
32
+ additions?: number;
33
+ deletions?: number;
34
+ commitMessages?: string[];
35
+ };
36
+ }
37
+
38
  interface UpdateInfo {
39
  currentVersion: string;
40
  latestVersion: string;
 
45
  changelog?: string[];
46
  currentCommit?: string;
47
  latestCommit?: string;
48
+ updateProgress?: UpdateProgress;
 
 
49
  error?: {
50
  type: string;
51
  message: string;
 
58
  checkInterval: number;
59
  }
60
 
 
 
 
 
 
 
 
61
  const categorizeChangelog = (messages: string[]) => {
62
  const categories = new Map<string, string[]>();
63
 
 
172
  const [isChecking, setIsChecking] = useState(false);
173
  const [isUpdating, setIsUpdating] = useState(false);
174
  const [error, setError] = useState<string | null>(null);
 
175
  const [showChangelog, setShowChangelog] = useState(false);
176
  const [showManualInstructions, setShowManualInstructions] = useState(false);
177
  const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
 
189
  const [lastChecked, setLastChecked] = useState<Date | null>(null);
190
  const [showUpdateDialog, setShowUpdateDialog] = useState(false);
191
  const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
192
+ const [updateProgress, setUpdateProgress] = useState<UpdateProgress | null>(null);
193
 
194
  useEffect(() => {
195
  localStorage.setItem('update_settings', JSON.stringify(updateSettings));
 
263
  const initiateUpdate = async () => {
264
  setIsUpdating(true);
265
  setError(null);
266
+ setUpdateProgress(null);
267
 
268
+ try {
269
+ const response = await fetch('/api/update', {
270
+ method: 'POST',
271
+ headers: {
272
+ 'Content-Type': 'application/json',
273
+ },
274
+ body: JSON.stringify({
275
+ branch: isLatestBranch ? 'main' : 'stable',
276
+ }),
277
+ });
278
+
279
+ if (!response.ok) {
280
+ const errorData = (await response.json()) as { error: string };
281
+ throw new Error(errorData.error || 'Failed to initiate update');
282
+ }
283
 
284
+ // Handle streaming response
285
+ const reader = response.body?.getReader();
 
 
286
 
287
+ if (!reader) {
288
+ throw new Error('Failed to read response stream');
289
+ }
290
 
291
+ const decoder = new TextDecoder();
292
+
293
+ while (true) {
294
+ const { done, value } = await reader.read();
 
295
 
296
+ if (done) {
297
+ break;
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
 
300
+ const chunk = decoder.decode(value);
301
+ const updates = chunk.split('\n').filter(Boolean);
 
302
 
303
+ for (const update of updates) {
304
+ try {
305
+ const progress = JSON.parse(update) as UpdateProgress;
306
+ setUpdateProgress(progress);
307
 
308
+ if (progress.error) {
309
+ throw new Error(progress.error);
310
+ }
 
 
311
 
312
+ if (progress.stage === 'complete') {
313
+ logStore.logSuccess('Update completed', {
314
+ type: 'update',
315
+ message: progress.message,
316
+ });
317
+ toast.success(progress.message);
318
+ setUpdateFailed(false);
319
 
320
+ return;
321
+ }
322
+
323
+ logStore.logInfo(`Update progress: ${progress.stage}`, {
324
+ type: 'update',
325
+ message: progress.message,
326
+ });
327
+ } catch (e) {
328
+ console.error('Failed to parse update progress:', e);
329
+ }
330
+ }
331
  }
332
+ } catch (err) {
333
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
334
+ setError('Failed to complete update. Please try again or update manually.');
335
+ console.error('Update failed:', err);
336
+ logStore.logSystem('Update failed: ' + errorMessage);
337
+ toast.error('Update failed: ' + errorMessage);
338
+ setUpdateFailed(true);
339
+ } finally {
340
+ setIsUpdating(false);
341
+ }
342
+ };
343
 
344
+ const handleRestart = async () => {
345
+ // Show confirmation dialog
346
+ if (window.confirm('The application needs to restart to apply the update. Proceed?')) {
347
+ // Save any necessary state
348
+ localStorage.setItem('pendingRestart', 'true');
349
+
350
+ // Reload the page
351
+ window.location.reload();
352
+ }
353
  };
354
 
355
+ // Check for pending restart on mount
356
+ useEffect(() => {
357
+ const pendingRestart = localStorage.getItem('pendingRestart');
358
+
359
+ if (pendingRestart === 'true') {
360
+ localStorage.removeItem('pendingRestart');
361
+ toast.success('Update applied successfully!');
362
+ }
363
+ }, []);
364
+
365
  useEffect(() => {
366
  const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
367
  const intervalId = setInterval(checkForUpdates, checkInterval);
 
772
  )}
773
 
774
  {/* Update Progress */}
775
+ {isUpdating && updateProgress && (
776
  <motion.div
777
  className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
778
  initial={{ opacity: 0, y: 20 }}
 
781
  >
782
  <div className="space-y-4">
783
  <div className="flex items-center justify-between">
784
+ <div>
785
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">
786
+ {updateProgress.stage.charAt(0).toUpperCase() + updateProgress.stage.slice(1)}
787
+ </span>
788
+ <p className="text-xs text-bolt-elements-textSecondary">{updateProgress.message}</p>
789
+ </div>
790
+ {updateProgress.progress !== undefined && (
791
+ <span className="text-sm text-bolt-elements-textSecondary">{Math.round(updateProgress.progress)}%</span>
792
+ )}
 
793
  </div>
794
+
795
+ {/* Show detailed information when available */}
796
+ {updateProgress.details && (
797
+ <div className="mt-4 space-y-4">
798
+ {updateProgress.details.commitMessages && updateProgress.details.commitMessages.length > 0 && (
799
+ <div>
800
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Commits to be applied:</h4>
801
+ <div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[200px] overflow-y-auto text-sm">
802
+ {updateProgress.details.commitMessages.map((msg, i) => (
803
+ <div key={i} className="text-bolt-elements-textSecondary py-1">
804
+ {msg}
805
+ </div>
806
+ ))}
807
+ </div>
808
+ </div>
809
+ )}
810
+
811
+ {updateProgress.details.changedFiles && updateProgress.details.changedFiles.length > 0 && (
812
+ <div>
813
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Changed Files:</h4>
814
+ <div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[200px] overflow-y-auto text-sm">
815
+ {updateProgress.details.changedFiles.map((file, i) => (
816
+ <div key={i} className="text-bolt-elements-textSecondary py-1">
817
+ {file}
818
+ </div>
819
+ ))}
820
+ </div>
821
+ </div>
822
+ )}
823
+
824
+ {(updateProgress.details.additions !== undefined || updateProgress.details.deletions !== undefined) && (
825
+ <div className="flex gap-4">
826
+ {updateProgress.details.additions !== undefined && (
827
+ <div className="text-green-500">
828
+ <span className="text-sm">+{updateProgress.details.additions} additions</span>
829
+ </div>
830
+ )}
831
+ {updateProgress.details.deletions !== undefined && (
832
+ <div className="text-red-500">
833
+ <span className="text-sm">-{updateProgress.details.deletions} deletions</span>
834
+ </div>
835
+ )}
836
+ </div>
837
+ )}
838
+ </div>
839
+ )}
840
+
841
+ {updateProgress.progress !== undefined && (
842
+ <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
843
+ <div
844
+ className="h-full bg-purple-500 transition-all duration-300"
845
+ style={{ width: `${updateProgress.progress}%` }}
846
+ />
847
+ </div>
848
+ )}
849
+
850
+ {/* Show restart button when update is complete */}
851
+ {updateProgress.stage === 'complete' && !updateProgress.error && (
852
+ <div className="mt-4 flex justify-end">
853
+ <button
854
+ onClick={handleRestart}
855
+ className="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
856
+ >
857
+ Restart Application
858
+ </button>
859
+ </div>
860
+ )}
861
  </div>
862
  </motion.div>
863
  )}
app/routes/api.update.ts CHANGED
@@ -1,10 +1,27 @@
1
  import { json } from '@remix-run/node';
2
  import type { ActionFunction } from '@remix-run/node';
 
 
 
 
3
 
4
  interface UpdateRequestBody {
5
  branch: string;
6
  }
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  export const action: ActionFunction = async ({ request }) => {
9
  if (request.method !== 'POST') {
10
  return json({ error: 'Method not allowed' }, { status: 405 });
@@ -13,24 +30,135 @@ export const action: ActionFunction = async ({ request }) => {
13
  try {
14
  const body = await request.json();
15
 
16
- // Type guard to check if body has the correct shape
17
  if (!body || typeof body !== 'object' || !('branch' in body) || typeof body.branch !== 'string') {
18
  return json({ error: 'Invalid request body: branch is required and must be a string' }, { status: 400 });
19
  }
20
 
21
  const { branch } = body as UpdateRequestBody;
22
 
23
- // Instead of direct Git operations, we'll return instructions
24
- return json({
25
- success: true,
26
- message: 'Please update manually using the following steps:',
27
- instructions: [
28
- `1. git fetch origin ${branch}`,
29
- `2. git pull origin ${branch}`,
30
- '3. pnpm install',
31
- '4. pnpm build',
32
- '5. Restart the application',
33
- ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  });
35
  } catch (error) {
36
  console.error('Update preparation failed:', error);
 
1
  import { json } from '@remix-run/node';
2
  import type { ActionFunction } from '@remix-run/node';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+
6
+ const execAsync = promisify(exec);
7
 
8
  interface UpdateRequestBody {
9
  branch: string;
10
  }
11
 
12
+ interface UpdateProgress {
13
+ stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
14
+ message: string;
15
+ progress?: number;
16
+ error?: string;
17
+ details?: {
18
+ changedFiles?: string[];
19
+ additions?: number;
20
+ deletions?: number;
21
+ commitMessages?: string[];
22
+ };
23
+ }
24
+
25
  export const action: ActionFunction = async ({ request }) => {
26
  if (request.method !== 'POST') {
27
  return json({ error: 'Method not allowed' }, { status: 405 });
 
30
  try {
31
  const body = await request.json();
32
 
 
33
  if (!body || typeof body !== 'object' || !('branch' in body) || typeof body.branch !== 'string') {
34
  return json({ error: 'Invalid request body: branch is required and must be a string' }, { status: 400 });
35
  }
36
 
37
  const { branch } = body as UpdateRequestBody;
38
 
39
+ // Create a ReadableStream to send progress updates
40
+ const stream = new ReadableStream({
41
+ async start(controller) {
42
+ const encoder = new TextEncoder();
43
+ const sendProgress = (update: UpdateProgress) => {
44
+ controller.enqueue(encoder.encode(JSON.stringify(update) + '\n'));
45
+ };
46
+
47
+ try {
48
+ // Fetch stage
49
+ sendProgress({
50
+ stage: 'fetch',
51
+ message: 'Fetching latest changes...',
52
+ progress: 0,
53
+ });
54
+
55
+ // Get current commit hash
56
+ const { stdout: currentCommit } = await execAsync('git rev-parse HEAD');
57
+
58
+ // Fetch changes
59
+ await execAsync('git fetch origin');
60
+
61
+ // Get list of changed files
62
+ const { stdout: diffOutput } = await execAsync(`git diff --name-status origin/${branch}`);
63
+ const changedFiles = diffOutput
64
+ .split('\n')
65
+ .filter(Boolean)
66
+ .map((line) => {
67
+ const [status, file] = line.split('\t');
68
+ return `${status === 'M' ? 'Modified' : status === 'A' ? 'Added' : 'Deleted'}: ${file}`;
69
+ });
70
+
71
+ // Get commit messages
72
+ const { stdout: logOutput } = await execAsync(`git log --oneline ${currentCommit.trim()}..origin/${branch}`);
73
+ const commitMessages = logOutput.split('\n').filter(Boolean);
74
+
75
+ // Get diff stats
76
+ const { stdout: diffStats } = await execAsync(`git diff --shortstat origin/${branch}`);
77
+ const stats = diffStats.match(
78
+ /(\d+) files? changed(?:, (\d+) insertions?\\(\\+\\))?(?:, (\d+) deletions?\\(-\\))?/,
79
+ );
80
+
81
+ sendProgress({
82
+ stage: 'fetch',
83
+ message: 'Changes detected',
84
+ progress: 100,
85
+ details: {
86
+ changedFiles,
87
+ additions: stats?.[2] ? parseInt(stats[2]) : 0,
88
+ deletions: stats?.[3] ? parseInt(stats[3]) : 0,
89
+ commitMessages,
90
+ },
91
+ });
92
+
93
+ // Pull stage
94
+ sendProgress({
95
+ stage: 'pull',
96
+ message: `Pulling changes from ${branch}...`,
97
+ progress: 0,
98
+ });
99
+
100
+ await execAsync(`git pull origin ${branch}`);
101
+
102
+ sendProgress({
103
+ stage: 'pull',
104
+ message: 'Changes pulled successfully',
105
+ progress: 100,
106
+ });
107
+
108
+ // Install stage
109
+ sendProgress({
110
+ stage: 'install',
111
+ message: 'Installing dependencies...',
112
+ progress: 0,
113
+ });
114
+
115
+ await execAsync('pnpm install');
116
+
117
+ sendProgress({
118
+ stage: 'install',
119
+ message: 'Dependencies installed successfully',
120
+ progress: 100,
121
+ });
122
+
123
+ // Build stage
124
+ sendProgress({
125
+ stage: 'build',
126
+ message: 'Building application...',
127
+ progress: 0,
128
+ });
129
+
130
+ await execAsync('pnpm build');
131
+
132
+ sendProgress({
133
+ stage: 'build',
134
+ message: 'Build completed successfully',
135
+ progress: 100,
136
+ });
137
+
138
+ // Complete
139
+ sendProgress({
140
+ stage: 'complete',
141
+ message: 'Update completed successfully! Click Restart to apply changes.',
142
+ progress: 100,
143
+ });
144
+ } catch (error) {
145
+ sendProgress({
146
+ stage: 'complete',
147
+ message: 'Update failed',
148
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
149
+ });
150
+ } finally {
151
+ controller.close();
152
+ }
153
+ },
154
+ });
155
+
156
+ return new Response(stream, {
157
+ headers: {
158
+ 'Content-Type': 'text/event-stream',
159
+ 'Cache-Control': 'no-cache',
160
+ Connection: 'keep-alive',
161
+ },
162
  });
163
  } catch (error) {
164
  console.error('Update preparation failed:', error);