From 6177ccd3335e7e43a073994dbc5ed5133cd0fae7 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:54:33 +0000 Subject: [PATCH] update notif (#4937) # Description of Changes image image image --- ## 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. --- .../software/SPDF/config/WebMvcConfig.java | 48 +- .../controller/api/misc/ConfigController.java | 19 + .../public/locales/en-GB/translation.json | 41 +- .../core/components/shared/UpdateModal.tsx | 415 ++++++++++++++++++ .../config/configSections/GeneralSection.tsx | 138 +++++- .../src/core/contexts/AppConfigContext.tsx | 3 + frontend/src/core/services/updateService.ts | 186 ++++++++ frontend/src/core/styles/zIndex.ts | 2 +- 8 files changed, 844 insertions(+), 8 deletions(-) create mode 100644 frontend/src/core/components/shared/UpdateModal.tsx create mode 100644 frontend/src/core/services/updateService.ts diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 0823c29e9..b6be63ec5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -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) } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index ffbec5a7d..a4a169cd9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -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) { diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index c9cd9c692..598ea2ee0 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -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", diff --git a/frontend/src/core/components/shared/UpdateModal.tsx b/frontend/src/core/components/shared/UpdateModal.tsx new file mode 100644 index 000000000..937f4426d --- /dev/null +++ b/frontend/src/core/components/shared/UpdateModal.tsx @@ -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 = ({ + opened, + onClose, + currentVersion, + updateSummary, + machineInfo, +}) => { + const { t } = useTranslation(); + const [fullUpdateInfo, setFullUpdateInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [expandedVersions, setExpandedVersions] = useState>(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 ( + + {t('update.modalTitle', 'Update Available')} + + } + centered + size="xl" + zIndex={Z_INDEX_OVER_CONFIG_MODAL} + styles={{ + body: { + maxHeight: '75vh', + overflowY: 'auto', + }, + }} + > + + {/* Version Summary Section */} + + + + + {t('update.current', 'Current Version')} + + + {currentVersion} + + + + + + {t('update.priorityLabel', 'Priority')} + + + {getPriorityLabel(updateSummary.max_priority)} + + + + + + {t('update.latest', 'Latest Version')} + + + {updateSummary.latest_version} + + + + + {updateSummary.latest_stable_version && ( + + + + {t('update.latestStable', 'Latest Stable')}: + + + {updateSummary.latest_stable_version} + + + + )} + + + {/* Recommended action */} + {updateSummary.recommended_action && ( + + + + + + {t('update.recommendedAction', 'Recommended Action')} + + + {updateSummary.recommended_action} + + + + + )} + + {/* Breaking changes warning */} + {updateSummary.any_breaking && ( + + + + + + {t('update.breakingChangesDetected', 'Breaking Changes Detected')} + + + {t( + 'update.breakingChangesMessage', + 'Some versions contain breaking changes. Please review the migration guides below before updating.' + )} + + + + + )} + + {/* Migration guides */} + {updateSummary.migration_guides && updateSummary.migration_guides.length > 0 && ( + <> + + + + {t('update.migrationGuides', 'Migration Guides')} + + {updateSummary.migration_guides.map((guide, idx) => ( + + + + + {t('update.version', 'Version')} {guide.version} + + + {guide.notes} + + + + + + ))} + + + )} + + {/* Version details */} + + {loading ? ( +
+ + + + {t('update.loadingDetailedInfo', 'Loading detailed information...')} + + +
+ ) : fullUpdateInfo && fullUpdateInfo.new_versions && fullUpdateInfo.new_versions.length > 0 ? ( + + + + {t('update.availableUpdates', 'Available Updates')} + + + {fullUpdateInfo.new_versions.length} {fullUpdateInfo.new_versions.length === 1 ? 'version' : 'versions'} + + + + {fullUpdateInfo.new_versions.map((version, index) => { + const isExpanded = expandedVersions.has(index); + return ( + + toggleVersion(index)} + > + + + + {t('update.version', 'Version')} + + + {version.version} + + + + {getPriorityLabel(version.priority)} + + + + + {isExpanded ? ( + + ) : ( + + )} + + + + + + + + + {version.announcement.title} + + + {version.announcement.message} + + + + {version.compatibility.breaking_changes && ( + + + + + {t('update.breakingChanges', 'Breaking Changes')} + + + + {version.compatibility.breaking_description || + t('update.breakingChangesDefault', 'This version contains breaking changes.')} + + {version.compatibility.migration_guide_url && ( + + )} + + )} + + + + + ); + })} + + + ) : null} + + {/* Action buttons */} + + + + + {downloadUrl && ( + + )} + +
+
+ ); +}; + +export default UpdateModal; diff --git a/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx b/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx index de9994705..e80a1fb07 100644 --- a/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx @@ -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 = ({ hideTitle = false }) => // Check localStorage on mount return localStorage.getItem(BANNER_DISMISSED_KEY) === 'true'; }); + const [updateSummary, setUpdateSummary] = useState(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 = ({ hideTitle = false }) => + + {/* Update Check Section */} + {config?.appVersion && ( + + +
+ +
+ + {t('settings.general.updates.title', 'Software Updates')} + + + {t('settings.general.updates.description', 'Check for updates and view version information')} + +
+ {updateSummary && ( + + {updateSummary.max_priority === 'urgent' + ? t('update.urgentUpdateAvailable', 'Urgent Update') + : t('update.updateAvailable', 'Update Available')} + + )} +
+
+ + +
+ + {t('settings.general.updates.currentVersion', 'Current Version')}:{' '} + + {config.appVersion} + + + {updateSummary && ( + + {t('settings.general.updates.latestVersion', 'Latest Version')}:{' '} + + {updateSummary.latest_version} + + + )} +
+ + + {updateSummary && ( + + )} + +
+ + {updateSummary?.any_breaking && ( + + + {t( + 'update.breakingChangesMessage', + 'Some versions contain breaking changes. Please review the migration guides before updating.' + )} + + + )} +
+
+ )} + + {/* Update Modal */} + {updateSummary && config?.appVersion && config?.machineType && ( + setUpdateModalOpened(false)} + currentVersion={config.appVersion} + updateSummary={updateSummary} + machineInfo={{ + machineType: config.machineType, + activeSecurity: config.activeSecurity ?? false, + licenseType: config.license ?? 'NORMAL', + }} + /> + )} ); }; diff --git a/frontend/src/core/contexts/AppConfigContext.tsx b/frontend/src/core/contexts/AppConfigContext.tsx index ad47b415b..2f672da26 100644 --- a/frontend/src/core/contexts/AppConfigContext.tsx +++ b/frontend/src/core/contexts/AppConfigContext.tsx @@ -39,6 +39,9 @@ export interface AppConfig { license?: string; SSOAutoLogin?: boolean; serverCertificateEnabled?: boolean; + appVersion?: string; + machineType?: string; + activeSecurity?: boolean; error?: string; } diff --git a/frontend/src/core/services/updateService.ts b/frontend/src/core/services/updateService.ts new file mode 100644 index 000000000..1cba4838f --- /dev/null +++ b/frontend/src/core/services/updateService.ts @@ -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 { + // 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 { + // 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 { + 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(); diff --git a/frontend/src/core/styles/zIndex.ts b/frontend/src/core/styles/zIndex.ts index aeb69e9b5..1057bf2f2 100644 --- a/frontend/src/core/styles/zIndex.ts +++ b/frontend/src/core/styles/zIndex.ts @@ -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)