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 {
|
|
|
|
|
|
|
|
|
|
|
15 |
import { profileStore } from '~/lib/stores/profile';
|
16 |
-
import type { TabType, TabVisibilityConfig,
|
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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
93 |
-
const
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
};
|
113 |
-
});
|
114 |
|
115 |
-
// Add Tab Management tile
|
116 |
-
|
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 |
-
//
|
129 |
-
const notificationsDisabled = profile?.preferences?.notifications === false;
|
130 |
-
|
131 |
return tabConfiguration.userTabs
|
132 |
.filter((tab) => {
|
133 |
-
if (!tab
|
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:
|
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 |
-
{/*
|
342 |
-
<div className="flex items-center gap-6">
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
|
|
277 |
|
278 |
-
|
279 |
-
|
280 |
-
throw new Error(errorData.error || 'Failed to initiate update');
|
281 |
-
}
|
282 |
|
283 |
-
|
|
|
|
|
284 |
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
});
|
290 |
|
291 |
-
|
292 |
-
|
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 |
-
|
307 |
-
|
308 |
-
currentRetry++;
|
309 |
|
310 |
-
const
|
|
|
|
|
|
|
311 |
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
316 |
-
await attemptUpdate();
|
317 |
|
318 |
-
|
319 |
-
|
|
|
|
|
|
|
|
|
|
|
320 |
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
}
|
327 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
|
329 |
-
|
330 |
-
|
331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 &&
|
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 |
-
<
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
className="
|
761 |
-
|
762 |
-
/>
|
763 |
</div>
|
764 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
//
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
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);
|