Skip to content

Commit 1b329c6

Browse files
authored
Merge pull request #87 from beNative/codex/enhance-auto-update-functionality-and-ui
Add auto-update preferences and in-app install controls
2 parents cfcdbaa + 97b1a1e commit 1b329c6

7 files changed

Lines changed: 397 additions & 67 deletions

File tree

App.tsx

Lines changed: 143 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ const App: React.FC = () => {
181181
const autoCheckIntervalRef = useRef<number | null>(null);
182182
const isAutoCheckingRef = useRef(false);
183183
const [updateReady, setUpdateReady] = useState(false);
184+
const [updateStatus, setUpdateStatus] = useState<UpdateStatusMessage | null>(null);
185+
const [isUpdateBannerVisible, setIsUpdateBannerVisible] = useState(false);
186+
const [autoInstallScheduled, setAutoInstallScheduled] = useState(false);
187+
const autoInstallTimeoutRef = useRef<number | null>(null);
184188

185189
// New states for deeper VCS integration
186190
const [detailedStatuses, setDetailedStatuses] = useState<Record<string, DetailedStatus | null>>({});
@@ -254,6 +258,70 @@ const App: React.FC = () => {
254258
onCancel: () => {},
255259
});
256260

261+
const cancelAutoInstall = useCallback(() => {
262+
if (autoInstallTimeoutRef.current) {
263+
window.clearTimeout(autoInstallTimeoutRef.current);
264+
autoInstallTimeoutRef.current = null;
265+
}
266+
setAutoInstallScheduled(false);
267+
}, []);
268+
269+
const handleRestartAndUpdate = useCallback(() => {
270+
cancelAutoInstall();
271+
if (window.electronAPI?.restartAndInstallUpdate) {
272+
setToast({ message: 'Restarting to install the latest update…', type: 'info' });
273+
window.electronAPI.restartAndInstallUpdate();
274+
} else {
275+
setToast({ message: 'Could not restart. Please restart the app manually.', type: 'error' });
276+
}
277+
}, [cancelAutoInstall, setToast]);
278+
279+
const scheduleAutoInstall = useCallback(() => {
280+
cancelAutoInstall();
281+
autoInstallTimeoutRef.current = window.setTimeout(() => {
282+
setAutoInstallScheduled(false);
283+
handleRestartAndUpdate();
284+
}, 8000);
285+
setAutoInstallScheduled(true);
286+
}, [cancelAutoInstall, handleRestartAndUpdate]);
287+
288+
const handleAutoInstallPreferenceChange = useCallback((mode: GlobalSettings['autoInstallUpdates']) => {
289+
if (mode === settings.autoInstallUpdates) {
290+
return;
291+
}
292+
saveSettings({ ...settings, autoInstallUpdates: mode });
293+
if (mode === 'manual') {
294+
cancelAutoInstall();
295+
setToast({
296+
message: 'Automatic installation disabled. Use the Update Ready controls when you want to apply the update.',
297+
type: 'info',
298+
});
299+
} else {
300+
setToast({
301+
message: 'Updates will now install automatically as soon as they finish downloading.',
302+
type: 'success',
303+
});
304+
}
305+
}, [cancelAutoInstall, saveSettings, settings, setToast]);
306+
307+
const handleDeferUpdate = useCallback(() => {
308+
if (settings.autoInstallUpdates === 'auto') {
309+
handleAutoInstallPreferenceChange('manual');
310+
}
311+
cancelAutoInstall();
312+
setIsUpdateBannerVisible(false);
313+
setToast({
314+
message: 'Update postponed. Use the “Update Ready” control in the title bar to install whenever you are ready.',
315+
type: 'info',
316+
});
317+
}, [cancelAutoInstall, handleAutoInstallPreferenceChange, settings.autoInstallUpdates, setToast]);
318+
319+
const handleShowUpdateDetails = useCallback(() => {
320+
if (updateReady) {
321+
setIsUpdateBannerVisible(true);
322+
}
323+
}, [updateReady]);
324+
257325
useEffect(() => {
258326
if (!instrumentation) {
259327
return;
@@ -467,31 +535,74 @@ const App: React.FC = () => {
467535
// Effect for auto-updater
468536
useEffect(() => {
469537
const handleUpdateStatus = (_event: any, data: UpdateStatusMessage) => {
470-
logger.info(`Update status change received: ${data.status}`, data);
471-
switch (data.status) {
472-
case 'available':
473-
setToast({ message: data.message, type: 'info' });
474-
break;
475-
case 'downloaded':
476-
setToast({ message: data.message, type: 'success' });
477-
setUpdateReady(true);
478-
break;
479-
case 'error':
480-
setToast({ message: data.message, type: 'error' });
481-
break;
482-
}
538+
logger.info(`Update status change received: ${data.status}`, data);
539+
setUpdateStatus(data);
540+
541+
switch (data.status) {
542+
case 'checking':
543+
cancelAutoInstall();
544+
setUpdateReady(false);
545+
setIsUpdateBannerVisible(false);
546+
break;
547+
case 'available':
548+
setToast({ message: data.message, type: 'info' });
549+
break;
550+
case 'downloaded':
551+
setUpdateReady(true);
552+
setIsUpdateBannerVisible(true);
553+
if (settings.autoInstallUpdates === 'auto') {
554+
setToast({ message: 'Update downloaded. Restarting automatically in a few seconds…', type: 'info' });
555+
scheduleAutoInstall();
556+
} else {
557+
cancelAutoInstall();
558+
setToast({ message: data.message, type: 'success' });
559+
}
560+
break;
561+
case 'error':
562+
cancelAutoInstall();
563+
setUpdateReady(false);
564+
setIsUpdateBannerVisible(false);
565+
setToast({ message: data.message, type: 'error' });
566+
break;
567+
default:
568+
break;
569+
}
483570
};
484571

485572
if (window.electronAPI?.onUpdateStatusChange) {
486-
window.electronAPI.onUpdateStatusChange(handleUpdateStatus);
573+
window.electronAPI.onUpdateStatusChange(handleUpdateStatus);
487574
}
488575

489576
return () => {
490-
if (window.electronAPI?.removeUpdateStatusChangeListener) {
491-
window.electronAPI.removeUpdateStatusChangeListener(handleUpdateStatus);
492-
}
577+
if (window.electronAPI?.removeUpdateStatusChangeListener) {
578+
window.electronAPI.removeUpdateStatusChangeListener(handleUpdateStatus);
579+
}
493580
};
494-
}, [logger]);
581+
}, [logger, cancelAutoInstall, scheduleAutoInstall, settings.autoInstallUpdates, setToast]);
582+
583+
useEffect(() => {
584+
if (!updateReady) {
585+
return;
586+
}
587+
588+
if (settings.autoInstallUpdates === 'auto') {
589+
if (!autoInstallScheduled) {
590+
scheduleAutoInstall();
591+
}
592+
} else if (autoInstallScheduled) {
593+
cancelAutoInstall();
594+
}
595+
}, [
596+
updateReady,
597+
settings.autoInstallUpdates,
598+
autoInstallScheduled,
599+
scheduleAutoInstall,
600+
cancelAutoInstall,
601+
]);
602+
603+
useEffect(() => () => {
604+
cancelAutoInstall();
605+
}, [cancelAutoInstall]);
495606

496607
// Effect to check local paths
497608
useEffect(() => {
@@ -1319,15 +1430,6 @@ const App: React.FC = () => {
13191430
}
13201431
}, []);
13211432

1322-
const handleRestartAndUpdate = useCallback(() => {
1323-
if (window.electronAPI?.restartAndInstallUpdate) {
1324-
window.electronAPI.restartAndInstallUpdate();
1325-
} else {
1326-
setToast({ message: 'Could not restart. Please restart the app manually.', type: 'error' });
1327-
}
1328-
}, []);
1329-
1330-
13311433
const latestLog = useMemo(() => {
13321434
const allLogs = Object.values(logs).flat();
13331435
if (allLogs.length === 0) return null;
@@ -1428,9 +1530,22 @@ const App: React.FC = () => {
14281530
isCheckingAll={isCheckingAll}
14291531
onToggleAllCategories={toggleAllCategoriesCollapse}
14301532
canCollapseAll={canCollapseAll}
1533+
updateReady={updateReady}
1534+
onInstallUpdate={handleRestartAndUpdate}
1535+
onShowUpdateDetails={handleShowUpdateDetails}
14311536
/>
14321537
<div className="flex-1 flex flex-col min-h-0">
1433-
{updateReady && <UpdateBanner onInstall={handleRestartAndUpdate} />}
1538+
{updateReady && isUpdateBannerVisible && updateStatus?.status === 'downloaded' && (
1539+
<UpdateBanner
1540+
version={updateStatus.version}
1541+
message={updateStatus.message}
1542+
autoInstallMode={settings.autoInstallUpdates}
1543+
autoInstallScheduled={autoInstallScheduled}
1544+
onChangeMode={handleAutoInstallPreferenceChange}
1545+
onInstallNow={handleRestartAndUpdate}
1546+
onInstallLater={handleDeferUpdate}
1547+
/>
1548+
)}
14341549
<main
14351550
className={mainContentClass}
14361551
data-automation-id="main-content"

components/Header.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ArrowsPointingOutIcon } from './icons/ArrowsPointingOutIcon';
1313
import { useSettings } from '../contexts/SettingsContext';
1414
import type { AppView } from '../types';
1515
import { useTooltip } from '../hooks/useTooltip';
16+
import { RocketLaunchIcon } from './icons/RocketLaunchIcon';
1617

1718
interface TitleBarProps {
1819
activeView: AppView;
@@ -22,6 +23,9 @@ interface TitleBarProps {
2223
isCheckingAll: boolean;
2324
onToggleAllCategories: () => void;
2425
canCollapseAll: boolean;
26+
updateReady: boolean;
27+
onInstallUpdate: () => void;
28+
onShowUpdateDetails: () => void;
2529
}
2630

2731
const TitleBar: React.FC<TitleBarProps> = ({
@@ -32,6 +36,9 @@ const TitleBar: React.FC<TitleBarProps> = ({
3236
isCheckingAll,
3337
onToggleAllCategories,
3438
canCollapseAll,
39+
updateReady,
40+
onInstallUpdate,
41+
onShowUpdateDetails,
3542
}) => {
3643
const { settings, saveSettings } = useSettings();
3744
const isEditing = activeView === 'edit-repository';
@@ -55,6 +62,8 @@ const TitleBar: React.FC<TitleBarProps> = ({
5562
const newRepoTooltip = useTooltip('Add New Repository');
5663
const checkUpdatesTooltip = useTooltip('Check all repositories for updates');
5764
const expandCollapseTooltip = useTooltip(canCollapseAll ? 'Collapse all categories' : 'Expand all categories');
65+
const installUpdateTooltip = useTooltip('Restart to apply the update now');
66+
const updateDetailsTooltip = useTooltip('Show update installation options');
5867

5968
const noDragStyle = { WebkitAppRegion: 'no-drag' } as React.CSSProperties;
6069

@@ -109,6 +118,36 @@ const TitleBar: React.FC<TitleBarProps> = ({
109118
)}
110119
</div>
111120
<div className="flex items-center">
121+
{updateReady && (
122+
<div
123+
className="flex items-center gap-2 pr-2 mr-2 rounded-md border border-green-400/40 bg-green-500/20 px-2 py-1 text-xs font-semibold uppercase tracking-widest text-green-100 shadow-sm"
124+
style={noDragStyle}
125+
data-automation-id="titlebar-update-ready"
126+
>
127+
<RocketLaunchIcon className="h-4 w-4" aria-hidden="true" />
128+
<span className="hidden sm:inline">Update Ready</span>
129+
<div className="flex items-center gap-1">
130+
<button
131+
{...installUpdateTooltip}
132+
onClick={onInstallUpdate}
133+
className="rounded bg-white/80 px-2 py-0.5 text-xs font-bold text-green-700 transition hover:bg-white"
134+
style={noDragStyle}
135+
data-automation-id="titlebar-update-install"
136+
>
137+
Install
138+
</button>
139+
<button
140+
{...updateDetailsTooltip}
141+
onClick={onShowUpdateDetails}
142+
className="rounded border border-white/60 px-2 py-0.5 text-xs font-bold text-white/90 transition hover:bg-white/20"
143+
style={noDragStyle}
144+
data-automation-id="titlebar-update-details"
145+
>
146+
Details
147+
</button>
148+
</div>
149+
</div>
150+
)}
112151
<div className="flex items-center gap-1 pr-1 border-r border-gray-300/60 dark:border-gray-700/60 mr-1" style={noDragStyle}>
113152
<button
114153
{...dashboardTooltip}

components/SettingsView.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,63 @@ const SettingsView: React.FC<SettingsViewProps> = ({ onSave, currentSettings, se
283283
</div>
284284
</div>
285285

286+
<div className="space-y-4 pt-6 border-t border-gray-200 dark:border-gray-700">
287+
<div className="flex flex-col gap-1">
288+
<span className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 dark:text-blue-400">Application Updates</span>
289+
<h4 className="text-lg font-semibold">Keep Git Automation Dashboard current</h4>
290+
<p className="text-sm text-gray-500 dark:text-gray-400">Control how the app checks for new releases and whether updates install themselves once downloaded.</p>
291+
</div>
292+
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
293+
<label className="flex items-start gap-3 rounded-lg border border-gray-200 bg-white/60 p-4 shadow-sm transition dark:border-gray-700 dark:bg-gray-900/50">
294+
<input
295+
type="checkbox"
296+
name="autoUpdateChecksEnabled"
297+
checked={settings.autoUpdateChecksEnabled}
298+
onChange={handleChange}
299+
className="mt-1 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"
300+
data-automation-id="settings-auto-update-checks"
301+
/>
302+
<div>
303+
<p className="font-medium text-gray-900 dark:text-gray-100">Enable automatic update checks</p>
304+
<p className="text-xs text-gray-500 dark:text-gray-400">When enabled, Git Automation Dashboard looks for the latest release whenever it starts.</p>
305+
</div>
306+
</label>
307+
<div className="rounded-lg border border-gray-200 bg-white/60 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900/50">
308+
<p className="text-xs font-semibold uppercase tracking-wide text-purple-600 dark:text-purple-300" data-automation-id="settings-install-label">Installation preference</p>
309+
<fieldset className="mt-3 space-y-3" data-automation-id="settings-auto-install-options">
310+
<label className="flex items-start gap-3">
311+
<input
312+
type="radio"
313+
name="autoInstallUpdates"
314+
value="auto"
315+
checked={settings.autoInstallUpdates === 'auto'}
316+
onChange={handleChange}
317+
className="mt-1 focus:ring-purple-500 h-4 w-4 text-purple-600 border-gray-300 dark:border-gray-600"
318+
/>
319+
<div>
320+
<p className="font-medium text-gray-900 dark:text-gray-100">Install automatically after download</p>
321+
<p className="text-xs text-gray-500 dark:text-gray-400">The app restarts itself to finish installing as soon as an update is ready.</p>
322+
</div>
323+
</label>
324+
<label className="flex items-start gap-3">
325+
<input
326+
type="radio"
327+
name="autoInstallUpdates"
328+
value="manual"
329+
checked={settings.autoInstallUpdates === 'manual'}
330+
onChange={handleChange}
331+
className="mt-1 focus:ring-purple-500 h-4 w-4 text-purple-600 border-gray-300 dark:border-gray-600"
332+
/>
333+
<div>
334+
<p className="font-medium text-gray-900 dark:text-gray-100">I'll install updates manually</p>
335+
<p className="text-xs text-gray-500 dark:text-gray-400">Keep working and choose when to restart. You'll still see reminders when an update is ready.</p>
336+
</div>
337+
</label>
338+
</fieldset>
339+
</div>
340+
</div>
341+
</div>
342+
286343
<div className="space-y-4 pt-6 border-t border-gray-200 dark:border-gray-700">
287344
<h4 className="text-lg font-semibold">Automatic Update Checks</h4>
288345
<label className="flex items-start gap-3">

0 commit comments

Comments
 (0)