update notif (#4937)

# Description of Changes
<img width="1521" height="1041" alt="image"
src="https://github.com/user-attachments/assets/2644bf70-0a9b-4c91-9046-02f555314608"
/>

<img width="1162" height="1598" alt="image"
src="https://github.com/user-attachments/assets/36d693f7-6fdd-4f2b-9db1-39ac336d9055"
/>

<img width="1220" height="1625" alt="image"
src="https://github.com/user-attachments/assets/4d4c19ea-0020-45fb-b15a-9f6ad377856c"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Anthony Stirling 2025-11-19 11:54:33 +00:00 committed by GitHub
parent 87bf7a5b7f
commit 6177ccd333
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 844 additions and 8 deletions

View File

@ -63,13 +63,51 @@ public class WebMvcConfig implements WebMvcConfigurer {
.toArray(new String[0]);
registry.addMapping("/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowedOriginPatterns(allowedOrigins)
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"X-API-KEY",
"X-CSRF-TOKEN",
"X-XSRF-TOKEN")
.exposedHeaders(
"WWW-Authenticate",
"X-Total-Count",
"X-Page-Number",
"X-Page-Size",
"Content-Disposition",
"Content-Type")
.allowCredentials(true)
.maxAge(3600);
} else {
// Default to allowing all origins when nothing is configured
logger.info(
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"X-API-KEY",
"X-CSRF-TOKEN",
"X-XSRF-TOKEN")
.exposedHeaders(
"WWW-Authenticate",
"X-Total-Count",
"X-Page-Number",
"X-Page-Size",
"Content-Disposition",
"Content-Type")
.allowCredentials(true)
.maxAge(3600);
}
// If no origins are configured and not in Tauri mode, CORS is not enabled (secure by
// default)
}
}

View File

@ -154,6 +154,25 @@ public class ConfigController {
// EE features not available, continue without them
}
// Add version and machine info for update checking
try {
if (applicationContext.containsBean("appVersion")) {
configData.put(
"appVersion", applicationContext.getBean("appVersion", String.class));
}
if (applicationContext.containsBean("machineType")) {
configData.put(
"machineType", applicationContext.getBean("machineType", String.class));
}
if (applicationContext.containsBean("activeSecurity")) {
configData.put(
"activeSecurity",
applicationContext.getBean("activeSecurity", Boolean.class));
}
} catch (Exception e) {
// Version/machine info not available
}
return ResponseEntity.ok(configData);
} catch (Exception e) {

View File

@ -362,7 +362,15 @@
"defaultPdfEditorInactive": "Another application is set as default",
"defaultPdfEditorChecking": "Checking...",
"defaultPdfEditorSet": "Already Default",
"setAsDefault": "Set as Default"
"setAsDefault": "Set as Default",
"updates": {
"title": "Software Updates",
"description": "Check for updates and view version information",
"currentVersion": "Current Version",
"latestVersion": "Latest Version",
"checkForUpdates": "Check for Updates",
"viewDetails": "View Details"
}
},
"hotkeys": {
"title": "Keyboard Shortcuts",
@ -383,6 +391,37 @@
"searchPlaceholder": "Search tools..."
}
},
"update": {
"modalTitle": "Update Available",
"current": "Current Version",
"latest": "Latest Version",
"latestStable": "Latest Stable",
"priorityLabel": "Priority",
"recommendedAction": "Recommended Action",
"breakingChangesDetected": "Breaking Changes Detected",
"breakingChangesMessage": "Some versions contain breaking changes. Please review the migration guides below before updating.",
"migrationGuides": "Migration Guides",
"viewGuide": "View Guide",
"loadingDetailedInfo": "Loading detailed information...",
"close": "Close",
"viewAllReleases": "View All Releases",
"downloadLatest": "Download Latest",
"availableUpdates": "Available Updates",
"unableToLoadDetails": "Unable to load detailed information.",
"version": "Version",
"urgentUpdateAvailable": "Urgent Update",
"updateAvailable": "Update Available",
"releaseNotes": "Release Notes",
"priority": {
"urgent": "Urgent",
"normal": "Normal",
"minor": "Minor",
"low": "Low"
},
"breakingChanges": "Breaking Changes",
"breakingChangesDefault": "This version contains breaking changes.",
"migrationGuide": "Migration Guide"
},
"changeCreds": {
"title": "Change Credentials",
"header": "Update Your Account Details",

View File

@ -0,0 +1,415 @@
import React, { useState, useEffect } from 'react';
import { Modal, Stack, Text, Badge, Button, Group, Loader, Center, Divider, Box, Collapse } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { updateService, UpdateSummary, FullUpdateInfo, MachineInfo } from '@app/services/updateService';
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DownloadIcon from '@mui/icons-material/Download';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
interface UpdateModalProps {
opened: boolean;
onClose: () => void;
currentVersion: string;
updateSummary: UpdateSummary;
machineInfo: MachineInfo;
}
const UpdateModal: React.FC<UpdateModalProps> = ({
opened,
onClose,
currentVersion,
updateSummary,
machineInfo,
}) => {
const { t } = useTranslation();
const [fullUpdateInfo, setFullUpdateInfo] = useState<FullUpdateInfo | null>(null);
const [loading, setLoading] = useState(true);
const [expandedVersions, setExpandedVersions] = useState<Set<number>>(new Set([0]));
useEffect(() => {
if (opened) {
setLoading(true);
setExpandedVersions(new Set([0]));
updateService.getFullUpdateInfo(currentVersion, machineInfo).then((info) => {
setFullUpdateInfo(info);
setLoading(false);
});
}
}, [opened, currentVersion, machineInfo]);
const toggleVersion = (index: number) => {
setExpandedVersions((prev) => {
const newSet = new Set(prev);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
return newSet;
});
};
const getPriorityColor = (priority: string): string => {
switch (priority?.toLowerCase()) {
case 'urgent':
return 'red';
case 'normal':
return 'blue';
case 'minor':
return 'cyan';
case 'low':
return 'gray';
default:
return 'gray';
}
};
const getPriorityLabel = (priority: string): string => {
const key = priority?.toLowerCase();
return t(`update.priority.${key}`, priority || 'Normal');
};
const downloadUrl = updateService.getDownloadUrl(machineInfo);
return (
<Modal
opened={opened}
onClose={onClose}
title={
<Text fw={600} size="lg">
{t('update.modalTitle', 'Update Available')}
</Text>
}
centered
size="xl"
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
styles={{
body: {
maxHeight: '75vh',
overflowY: 'auto',
},
}}
>
<Stack gap="lg" pt="md">
{/* Version Summary Section */}
<Box>
<Group justify="space-between" align="flex-start" wrap="nowrap" mb="md">
<Stack gap={4} style={{ flex: 1 }}>
<Text size="xs" c="dimmed" tt="uppercase" fw={500}>
{t('update.current', 'Current Version')}
</Text>
<Text fw={600} size="xl">
{currentVersion}
</Text>
</Stack>
<Stack gap={4} style={{ flex: 1 }} ta="center">
<Text size="xs" c="dimmed" tt="uppercase" fw={500}>
{t('update.priorityLabel', 'Priority')}
</Text>
<Badge
color={getPriorityColor(updateSummary.max_priority)}
size="lg"
variant="filled"
style={{ alignSelf: 'center' }}
>
{getPriorityLabel(updateSummary.max_priority)}
</Badge>
</Stack>
<Stack gap={4} style={{ flex: 1 }} ta="right">
<Text size="xs" c="dimmed" tt="uppercase" fw={500}>
{t('update.latest', 'Latest Version')}
</Text>
<Text fw={600} size="xl" c="blue">
{updateSummary.latest_version}
</Text>
</Stack>
</Group>
{updateSummary.latest_stable_version && (
<Box
style={{
background: 'var(--mantine-color-green-0)',
padding: '10px 16px',
borderRadius: '8px',
border: '1px solid var(--mantine-color-green-2)',
}}
>
<Group gap="xs" justify="center">
<Text size="sm" fw={500}>
{t('update.latestStable', 'Latest Stable')}:
</Text>
<Text size="sm" fw={600} c="green">
{updateSummary.latest_stable_version}
</Text>
</Group>
</Box>
)}
</Box>
{/* Recommended action */}
{updateSummary.recommended_action && (
<Box
style={{
background: 'var(--mantine-color-blue-light)',
padding: '12px 16px',
borderRadius: '8px',
border: '1px solid var(--mantine-color-blue-outline)',
}}
>
<Group gap="xs" wrap="nowrap" align="flex-start">
<InfoOutlinedIcon style={{ fontSize: 18, color: 'var(--mantine-color-blue-filled)', marginTop: 2 }} />
<Box style={{ flex: 1 }}>
<Text size="xs" fw={600} mb={4} tt="uppercase">
{t('update.recommendedAction', 'Recommended Action')}
</Text>
<Text size="sm">
{updateSummary.recommended_action}
</Text>
</Box>
</Group>
</Box>
)}
{/* Breaking changes warning */}
{updateSummary.any_breaking && (
<Box
style={{
background: 'var(--mantine-color-orange-light)',
padding: '12px 16px',
borderRadius: '8px',
border: '1px solid var(--mantine-color-orange-outline)',
}}
>
<Group gap="xs" wrap="nowrap" align="flex-start">
<WarningAmberIcon style={{ fontSize: 18, color: 'var(--mantine-color-orange-filled)', marginTop: 2 }} />
<Box style={{ flex: 1 }}>
<Text size="xs" fw={600} mb={4} tt="uppercase">
{t('update.breakingChangesDetected', 'Breaking Changes Detected')}
</Text>
<Text size="sm">
{t(
'update.breakingChangesMessage',
'Some versions contain breaking changes. Please review the migration guides below before updating.'
)}
</Text>
</Box>
</Group>
</Box>
)}
{/* Migration guides */}
{updateSummary.migration_guides && updateSummary.migration_guides.length > 0 && (
<>
<Divider />
<Stack gap="xs">
<Text fw={600} size="sm" tt="uppercase" c="dimmed">
{t('update.migrationGuides', 'Migration Guides')}
</Text>
{updateSummary.migration_guides.map((guide, idx) => (
<Box
key={idx}
style={{
border: '1px solid var(--mantine-color-gray-3)',
padding: '12px 16px',
borderRadius: '8px',
background: 'var(--mantine-color-gray-0)',
}}
>
<Group justify="space-between" align="center" wrap="nowrap">
<Box style={{ flex: 1 }}>
<Text fw={600} size="sm">
{t('update.version', 'Version')} {guide.version}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{guide.notes}
</Text>
</Box>
<Button
component="a"
href={guide.url}
target="_blank"
variant="light"
size="xs"
rightSection={<OpenInNewIcon style={{ fontSize: 14 }} />}
>
{t('update.viewGuide', 'View Guide')}
</Button>
</Group>
</Box>
))}
</Stack>
</>
)}
{/* Version details */}
<Divider />
{loading ? (
<Center py="xl">
<Stack align="center" gap="sm">
<Loader size="md" />
<Text size="sm" c="dimmed">
{t('update.loadingDetailedInfo', 'Loading detailed information...')}
</Text>
</Stack>
</Center>
) : fullUpdateInfo && fullUpdateInfo.new_versions && fullUpdateInfo.new_versions.length > 0 ? (
<Stack gap="xs">
<Group justify="space-between" align="center">
<Text fw={600} size="sm" tt="uppercase" c="dimmed">
{t('update.availableUpdates', 'Available Updates')}
</Text>
<Badge variant="light" color="gray">
{fullUpdateInfo.new_versions.length} {fullUpdateInfo.new_versions.length === 1 ? 'version' : 'versions'}
</Badge>
</Group>
<Stack gap="xs">
{fullUpdateInfo.new_versions.map((version, index) => {
const isExpanded = expandedVersions.has(index);
return (
<Box
key={index}
style={{
border: '1px solid var(--mantine-color-gray-3)',
borderRadius: '8px',
overflow: 'hidden',
}}
>
<Group
justify="space-between"
align="center"
p="md"
style={{
cursor: 'pointer',
background: isExpanded ? 'var(--mantine-color-gray-0)' : 'transparent',
transition: 'background 0.15s ease',
}}
onClick={() => toggleVersion(index)}
>
<Group gap="md" style={{ flex: 1 }}>
<Box>
<Text fw={600} size="sm" c="dimmed" mb={2}>
{t('update.version', 'Version')}
</Text>
<Text fw={700} size="lg">
{version.version}
</Text>
</Box>
<Badge color={getPriorityColor(version.priority)} size="md">
{getPriorityLabel(version.priority)}
</Badge>
</Group>
<Group gap="xs">
<Button
component="a"
href={`https://github.com/Stirling-Tools/Stirling-PDF/releases/tag/v${version.version}`}
target="_blank"
variant="light"
size="xs"
onClick={(e) => e.stopPropagation()}
rightSection={<OpenInNewIcon style={{ fontSize: 14 }} />}
>
{t('update.releaseNotes', 'Release Notes')}
</Button>
{isExpanded ? (
<ExpandLessIcon style={{ fontSize: 20, color: 'var(--mantine-color-gray-6)' }} />
) : (
<ExpandMoreIcon style={{ fontSize: 20, color: 'var(--mantine-color-gray-6)' }} />
)}
</Group>
</Group>
<Collapse in={isExpanded}>
<Box p="md" pt={0} style={{ borderTop: '1px solid var(--mantine-color-gray-2)' }}>
<Stack gap="md" mt="md">
<Box>
<Text fw={600} size="sm" mb={6}>
{version.announcement.title}
</Text>
<Text size="sm" c="dimmed" style={{ lineHeight: 1.6 }}>
{version.announcement.message}
</Text>
</Box>
{version.compatibility.breaking_changes && (
<Box
style={{
background: 'var(--mantine-color-orange-light)',
padding: '12px',
borderRadius: '6px',
border: '1px solid var(--mantine-color-orange-outline)',
}}
>
<Group gap="xs" align="flex-start" wrap="nowrap" mb="xs">
<WarningAmberIcon style={{ fontSize: 16, color: 'var(--mantine-color-orange-filled)', marginTop: 2 }} />
<Text size="xs" fw={600} tt="uppercase">
{t('update.breakingChanges', 'Breaking Changes')}
</Text>
</Group>
<Text size="sm" mb="xs">
{version.compatibility.breaking_description ||
t('update.breakingChangesDefault', 'This version contains breaking changes.')}
</Text>
{version.compatibility.migration_guide_url && (
<Button
component="a"
href={version.compatibility.migration_guide_url}
target="_blank"
variant="light"
color="orange"
size="xs"
rightSection={<OpenInNewIcon style={{ fontSize: 14 }} />}
>
{t('update.migrationGuide', 'Migration Guide')}
</Button>
)}
</Box>
)}
</Stack>
</Box>
</Collapse>
</Box>
);
})}
</Stack>
</Stack>
) : null}
{/* Action buttons */}
<Divider />
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={onClose}>
{t('update.close', 'Close')}
</Button>
<Button
variant="light"
component="a"
href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
target="_blank"
rightSection={<OpenInNewIcon style={{ fontSize: 16 }} />}
>
{t('update.viewAllReleases', 'View All Releases')}
</Button>
{downloadUrl && (
<Button
component="a"
href={downloadUrl}
target="_blank"
color="green"
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
>
{t('update.downloadLatest', 'Download Latest')}
</Button>
)}
</Group>
</Stack>
</Modal>
);
};
export default UpdateModal;

View File

@ -1,10 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl, Code, Group, Anchor, ActionIcon } from '@mantine/core';
import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl, Code, Group, Anchor, ActionIcon, Button, Badge, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import type { ToolPanelMode } from '@app/constants/toolPanel';
import LocalIcon from '@app/components/shared/LocalIcon';
import { updateService, UpdateSummary } from '@app/services/updateService';
import UpdateModal from '@app/components/shared/UpdateModal';
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
const BANNER_DISMISSED_KEY = 'stirlingpdf_features_banner_dismissed';
@ -22,12 +24,44 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
// Check localStorage on mount
return localStorage.getItem(BANNER_DISMISSED_KEY) === 'true';
});
const [updateSummary, setUpdateSummary] = useState<UpdateSummary | null>(null);
const [updateModalOpened, setUpdateModalOpened] = useState(false);
const [checkingUpdate, setCheckingUpdate] = useState(false);
// Sync local state with preference changes
useEffect(() => {
setFileLimitInput(preferences.autoUnzipFileLimit);
}, [preferences.autoUnzipFileLimit]);
// Check for updates on mount
useEffect(() => {
if (config?.appVersion && config?.machineType) {
checkForUpdate();
}
}, [config?.appVersion, config?.machineType]);
const checkForUpdate = async () => {
if (!config?.appVersion || !config?.machineType) {
return;
}
setCheckingUpdate(true);
const machineInfo = {
machineType: config.machineType,
activeSecurity: config.activeSecurity ?? false,
licenseType: config.license ?? 'NORMAL',
};
const summary = await updateService.getUpdateSummary(config.appVersion, machineInfo);
if (summary) {
const isNewerVersion = updateService.compareVersions(summary.latest_version, config.appVersion) > 0;
if (isNewerVersion) {
setUpdateSummary(summary);
}
}
setCheckingUpdate(false);
};
// Check if login is disabled
const loginDisabled = !config?.enableLogin;
@ -170,6 +204,108 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
</Tooltip>
</Stack>
</Paper>
{/* Update Check Section */}
{config?.appVersion && (
<Paper withBorder p="md" radius="md">
<Stack gap="md">
<div>
<Group justify="space-between" align="center">
<div>
<Text fw={600} size="sm">
{t('settings.general.updates.title', 'Software Updates')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t('settings.general.updates.description', 'Check for updates and view version information')}
</Text>
</div>
{updateSummary && (
<Badge
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
variant="filled"
>
{updateSummary.max_priority === 'urgent'
? t('update.urgentUpdateAvailable', 'Urgent Update')
: t('update.updateAvailable', 'Update Available')}
</Badge>
)}
</Group>
</div>
<Group justify="space-between" align="center">
<div>
<Text size="sm" c="dimmed">
{t('settings.general.updates.currentVersion', 'Current Version')}:{' '}
<Text component="span" fw={500}>
{config.appVersion}
</Text>
</Text>
{updateSummary && (
<Text size="sm" c="dimmed" mt={4}>
{t('settings.general.updates.latestVersion', 'Latest Version')}:{' '}
<Text component="span" fw={500} c="blue">
{updateSummary.latest_version}
</Text>
</Text>
)}
</div>
<Group gap="sm">
<Button
size="sm"
variant="default"
onClick={checkForUpdate}
loading={checkingUpdate}
leftSection={<LocalIcon icon="refresh-rounded" width="1rem" height="1rem" />}
>
{t('settings.general.updates.checkForUpdates', 'Check for Updates')}
</Button>
{updateSummary && (
<Button
size="sm"
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
onClick={() => setUpdateModalOpened(true)}
leftSection={<LocalIcon icon="system-update-rounded" width="1rem" height="1rem" />}
>
{t('settings.general.updates.viewDetails', 'View Details')}
</Button>
)}
</Group>
</Group>
{updateSummary?.any_breaking && (
<Alert
color="orange"
title={t('update.breakingChangesDetected', 'Breaking Changes Detected')}
styles={{
title: { fontWeight: 600 }
}}
>
<Text size="sm">
{t(
'update.breakingChangesMessage',
'Some versions contain breaking changes. Please review the migration guides before updating.'
)}
</Text>
</Alert>
)}
</Stack>
</Paper>
)}
{/* Update Modal */}
{updateSummary && config?.appVersion && config?.machineType && (
<UpdateModal
opened={updateModalOpened}
onClose={() => setUpdateModalOpened(false)}
currentVersion={config.appVersion}
updateSummary={updateSummary}
machineInfo={{
machineType: config.machineType,
activeSecurity: config.activeSecurity ?? false,
licenseType: config.license ?? 'NORMAL',
}}
/>
)}
</Stack>
);
};

View File

@ -39,6 +39,9 @@ export interface AppConfig {
license?: string;
SSOAutoLogin?: boolean;
serverCertificateEnabled?: boolean;
appVersion?: string;
machineType?: string;
activeSecurity?: boolean;
error?: string;
}

View File

@ -0,0 +1,186 @@
export interface UpdateSummary {
latest_version: string;
latest_stable_version?: string;
max_priority: 'urgent' | 'normal' | 'minor' | 'low';
recommended_action?: string;
any_breaking: boolean;
migration_guides?: Array<{
version: string;
notes: string;
url: string;
}>;
}
export interface VersionUpdate {
version: string;
priority: 'urgent' | 'normal' | 'minor' | 'low';
announcement: {
title: string;
message: string;
};
compatibility: {
breaking_changes: boolean;
breaking_description?: string;
migration_guide_url?: string;
};
}
export interface FullUpdateInfo {
latest_version: string;
latest_stable_version?: string;
new_versions: VersionUpdate[];
}
export interface MachineInfo {
machineType: string;
activeSecurity: boolean;
licenseType: string;
}
export class UpdateService {
private readonly baseUrl = 'https://supabase.stirling.com/functions/v1/updates';
/**
* Compare two version strings
* @returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal
*/
compareVersions(version1: string, version2: string): number {
const v1 = version1.split('.');
const v2 = version2.split('.');
for (let i = 0; i < v1.length || i < v2.length; i++) {
const n1 = parseInt(v1[i]) || 0;
const n2 = parseInt(v2[i]) || 0;
if (n1 > n2) {
return 1;
} else if (n1 < n2) {
return -1;
}
}
return 0;
}
/**
* Get download URL based on machine type and security settings
*/
getDownloadUrl(machineInfo: MachineInfo): string | null {
// Only show download for non-Docker installations
if (machineInfo.machineType === 'Docker' || machineInfo.machineType === 'Kubernetes') {
return null;
}
const baseUrl = 'https://files.stirlingpdf.com/';
// Determine file based on machine type and security
if (machineInfo.machineType === 'Server-jar') {
return baseUrl + (machineInfo.activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar');
}
// Client installations
if (machineInfo.machineType.startsWith('Client-')) {
const os = machineInfo.machineType.replace('Client-', ''); // win, mac, unix
const type = machineInfo.activeSecurity ? '-server-security' : '-server';
if (os === 'unix') {
return baseUrl + os + type + '.jar';
} else if (os === 'win') {
return baseUrl + os + '-installer.exe';
} else if (os === 'mac') {
return baseUrl + os + '-installer.dmg';
}
}
return null;
}
/**
* Fetch update summary from API
*/
async getUpdateSummary(currentVersion: string, machineInfo: MachineInfo): Promise<UpdateSummary | null> {
// Map Java License enum to API types
let type = 'normal';
if (machineInfo.licenseType === 'PRO') {
type = 'pro';
} else if (machineInfo.licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
const url = `${this.baseUrl}?from=${currentVersion}&type=${type}&login=${machineInfo.activeSecurity}&summary=true`;
console.log('Fetching update summary from:', url);
try {
const response = await fetch(url);
console.log('Response status:', response.status);
if (response.status === 200) {
const data = await response.json();
return data as UpdateSummary;
} else {
console.error('Failed to fetch update summary from Supabase:', response.status);
return null;
}
} catch (error) {
console.error('Failed to fetch update summary from Supabase:', error);
return null;
}
}
/**
* Fetch full update information with detailed version info
*/
async getFullUpdateInfo(currentVersion: string, machineInfo: MachineInfo): Promise<FullUpdateInfo | null> {
// Map Java License enum to API types
let type = 'normal';
if (machineInfo.licenseType === 'PRO') {
type = 'pro';
} else if (machineInfo.licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
const url = `${this.baseUrl}?from=${currentVersion}&type=${type}&login=${machineInfo.activeSecurity}&summary=false`;
console.log('Fetching full update info from:', url);
try {
const response = await fetch(url);
console.log('Full update response status:', response.status);
if (response.status === 200) {
const data = await response.json();
return data as FullUpdateInfo;
} else {
console.error('Failed to fetch full update info from Supabase:', response.status);
return null;
}
} catch (error) {
console.error('Failed to fetch full update info from Supabase:', error);
return null;
}
}
/**
* Get current version from GitHub build.gradle as fallback
*/
async getCurrentVersionFromGitHub(): Promise<string> {
const url = 'https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/master/build.gradle';
try {
const response = await fetch(url);
if (response.status === 200) {
const text = await response.text();
const versionRegex = /version\s*=\s*['"](\d+\.\d+\.\d+)['"]/;
const match = versionRegex.exec(text);
if (match) {
return match[1];
}
}
throw new Error('Version number not found');
} catch (error) {
console.error('Failed to fetch latest version from build.gradle:', error);
return '';
}
}
}
export const updateService = new UpdateService();

View File

@ -15,7 +15,7 @@ export const Z_INDEX_HOVER_ACTION_MENU = 100;
export const Z_INDEX_SELECTION_BOX = 1000;
export const Z_INDEX_DROP_INDICATOR = 1001;
export const Z_INDEX_DRAG_BADGE = 1001;
// Modal that appears on top of config modal (e.g., restart confirmation)
// Modal that appears on top of config modal (e.g., restart confirmation, update modal)
export const Z_INDEX_OVER_CONFIG_MODAL = 2000;
// Toast notifications and error displays - Always on top (higher than rainbow theme at 10000)