From ab362dfd9086a3f4288d945fd5da5f11f28e1c83 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:06:11 +0000 Subject: [PATCH 1/9] :globe_with_meridians: [V2] Sync Translations + Update README Progress Table (#4683) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation for the **V2 branch**. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`frontend/public/locales/*/translation.json`) to reflect changes in the reference file `en-GB/translation.json`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 74 +++++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 250f71abb..f8058d906 100644 --- a/README.md +++ b/README.md @@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages! | Language | Progress | | -------------------------------------------- | -------------------------------------- | -| Arabic (العربية) (ar_AR) | ![95%](https://geps.dev/progress/95) | -| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![35%](https://geps.dev/progress/35) | -| Basque (Euskara) (eu_ES) | ![20%](https://geps.dev/progress/20) | -| Bulgarian (Български) (bg_BG) | ![39%](https://geps.dev/progress/39) | -| Catalan (Català) (ca_CA) | ![37%](https://geps.dev/progress/37) | -| Croatian (Hrvatski) (hr_HR) | ![34%](https://geps.dev/progress/34) | -| Czech (Česky) (cs_CZ) | ![38%](https://geps.dev/progress/38) | -| Danish (Dansk) (da_DK) | ![34%](https://geps.dev/progress/34) | -| Dutch (Nederlands) (nl_NL) | ![34%](https://geps.dev/progress/34) | +| Arabic (العربية) (ar_AR) | ![83%](https://geps.dev/progress/83) | +| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![32%](https://geps.dev/progress/32) | +| Basque (Euskara) (eu_ES) | ![18%](https://geps.dev/progress/18) | +| Bulgarian (Български) (bg_BG) | ![35%](https://geps.dev/progress/35) | +| Catalan (Català) (ca_CA) | ![34%](https://geps.dev/progress/34) | +| Croatian (Hrvatski) (hr_HR) | ![31%](https://geps.dev/progress/31) | +| Czech (Česky) (cs_CZ) | ![34%](https://geps.dev/progress/34) | +| Danish (Dansk) (da_DK) | ![30%](https://geps.dev/progress/30) | +| Dutch (Nederlands) (nl_NL) | ![30%](https://geps.dev/progress/30) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | -| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) | -| German (Deutsch) (de_DE) | ![95%](https://geps.dev/progress/95) | -| Greek (Ελληνικά) (el_GR) | ![38%](https://geps.dev/progress/38) | -| Hindi (हिंदी) (hi_IN) | ![38%](https://geps.dev/progress/38) | -| Hungarian (Magyar) (hu_HU) | ![42%](https://geps.dev/progress/42) | -| Indonesian (Bahasa Indonesia) (id_ID) | ![34%](https://geps.dev/progress/34) | -| Irish (Gaeilge) (ga_IE) | ![38%](https://geps.dev/progress/38) | -| Italian (Italiano) (it_IT) | ![95%](https://geps.dev/progress/95) | -| Japanese (日本語) (ja_JP) | ![70%](https://geps.dev/progress/70) | -| Korean (한국어) (ko_KR) | ![38%](https://geps.dev/progress/38) | -| Norwegian (Norsk) (no_NB) | ![36%](https://geps.dev/progress/36) | -| Persian (فارسی) (fa_IR) | ![38%](https://geps.dev/progress/38) | -| Polish (Polski) (pl_PL) | ![40%](https://geps.dev/progress/40) | -| Portuguese (Português) (pt_PT) | ![38%](https://geps.dev/progress/38) | -| Portuguese Brazilian (Português) (pt_BR) | ![95%](https://geps.dev/progress/95) | -| Romanian (Română) (ro_RO) | ![32%](https://geps.dev/progress/32) | -| Russian (Русский) (ru_RU) | ![94%](https://geps.dev/progress/94) | -| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![42%](https://geps.dev/progress/42) | -| Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) | -| Slovakian (Slovensky) (sk_SK) | ![28%](https://geps.dev/progress/28) | -| Slovenian (Slovenščina) (sl_SI) | ![40%](https://geps.dev/progress/40) | -| Spanish (Español) (es_ES) | ![95%](https://geps.dev/progress/95) | -| Swedish (Svenska) (sv_SE) | ![37%](https://geps.dev/progress/37) | -| Thai (ไทย) (th_TH) | ![34%](https://geps.dev/progress/34) | +| French (Français) (fr_FR) | ![82%](https://geps.dev/progress/82) | +| German (Deutsch) (de_DE) | ![84%](https://geps.dev/progress/84) | +| Greek (Ελληνικά) (el_GR) | ![34%](https://geps.dev/progress/34) | +| Hindi (हिंदी) (hi_IN) | ![34%](https://geps.dev/progress/34) | +| Hungarian (Magyar) (hu_HU) | ![38%](https://geps.dev/progress/38) | +| Indonesian (Bahasa Indonesia) (id_ID) | ![31%](https://geps.dev/progress/31) | +| Irish (Gaeilge) (ga_IE) | ![34%](https://geps.dev/progress/34) | +| Italian (Italiano) (it_IT) | ![84%](https://geps.dev/progress/84) | +| Japanese (日本語) (ja_JP) | ![62%](https://geps.dev/progress/62) | +| Korean (한국어) (ko_KR) | ![34%](https://geps.dev/progress/34) | +| Norwegian (Norsk) (no_NB) | ![32%](https://geps.dev/progress/32) | +| Persian (فارسی) (fa_IR) | ![34%](https://geps.dev/progress/34) | +| Polish (Polski) (pl_PL) | ![36%](https://geps.dev/progress/36) | +| Portuguese (Português) (pt_PT) | ![34%](https://geps.dev/progress/34) | +| Portuguese Brazilian (Português) (pt_BR) | ![83%](https://geps.dev/progress/83) | +| Romanian (Română) (ro_RO) | ![28%](https://geps.dev/progress/28) | +| Russian (Русский) (ru_RU) | ![83%](https://geps.dev/progress/83) | +| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![37%](https://geps.dev/progress/37) | +| Simplified Chinese (简体中文) (zh_CN) | ![85%](https://geps.dev/progress/85) | +| Slovakian (Slovensky) (sk_SK) | ![26%](https://geps.dev/progress/26) | +| Slovenian (Slovenščina) (sl_SI) | ![36%](https://geps.dev/progress/36) | +| Spanish (Español) (es_ES) | ![84%](https://geps.dev/progress/84) | +| Swedish (Svenska) (sv_SE) | ![33%](https://geps.dev/progress/33) | +| Thai (ไทย) (th_TH) | ![31%](https://geps.dev/progress/31) | | Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) | -| Traditional Chinese (繁體中文) (zh_TW) | ![42%](https://geps.dev/progress/42) | -| Turkish (Türkçe) (tr_TR) | ![41%](https://geps.dev/progress/41) | -| Ukrainian (Українська) (uk_UA) | ![40%](https://geps.dev/progress/40) | -| Vietnamese (Tiếng Việt) (vi_VN) | ![31%](https://geps.dev/progress/31) | +| Traditional Chinese (繁體中文) (zh_TW) | ![38%](https://geps.dev/progress/38) | +| Turkish (Türkçe) (tr_TR) | ![37%](https://geps.dev/progress/37) | +| Ukrainian (Українська) (uk_UA) | ![36%](https://geps.dev/progress/36) | +| Vietnamese (Tiếng Việt) (vi_VN) | ![28%](https://geps.dev/progress/28) | | Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) | ## Stirling PDF Enterprise From c67859a1ff1ea57010b2437ff04e4419ada5d5b9 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Mon, 27 Oct 2025 16:05:24 +0000 Subject: [PATCH 2/9] Ban lint warnings (#4753) # Description of Changes [See my comment here on why I think we should never allow lint warnings to be merged into our source](https://github.com/Stirling-Tools/Stirling-PDF/pull/4738#issuecomment-3451053692). This doesn't change how ESLint behaves at all other than if only warnings are reported, it'll report failure instead of success. --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index c49b2297b..053a7d67b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,7 +59,7 @@ "predev": "npm run generate-icons", "dev": "npm run typecheck && vite", "prebuild": "npm run generate-icons", - "lint": "eslint", + "lint": "eslint --max-warnings=0", "build": "npm run typecheck && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", From 3e9c55243e418642db4a6e04447941a40482c4cb Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 27 Oct 2025 17:22:56 +0100 Subject: [PATCH 3/9] style(frontend): enforce semicolons across auth & shared components (#4737) # Description of Changes This pull request primarily focuses on code style improvements across several frontend files, standardizing the use of semicolons and ensuring consistent formatting. No functional or logic changes are introduced; the updates are purely syntactic to improve code readability and maintainability. **Code Style and Formatting Improvements:** * Added missing semicolons and standardized import statements in multiple files, including `DividerWithText.tsx`, `LoginRightCarousel.tsx`, `loginSlides.ts`, `AuthCallback.tsx`, `Landing.tsx`, `Login.tsx`, and `Signup.tsx`. [[1]](diffhunk://#diff-5de1b22e63fe3b6c9781c2a476db7440818f18d2aeb5c6c1ddeb446517cf001fL1-R1) [[2]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L1-R1) [[3]](diffhunk://#diff-1fc806abd10f8882945f54b56828db4c4b9a8b986743250b26dd9bdf0ec49bdbL41-R43) [[4]](diffhunk://#diff-540ce2405611334ce0bdff1f48d187218be99ce64fb92f054b9cf5a71cb1ed8cL1-R3) [[5]](diffhunk://#diff-d55dde4f28998eb9b30f332a1c96a4c79ec6a70b568bb51eea81d11a3715c35cL1-R5) [[6]](diffhunk://#diff-183a38f7c78b7c2950c4bed87ff2843de146d960e28591865d91c3cd86c3fadbL1-R39) [[7]](diffhunk://#diff-0a98c2e661e58f226f98c90b2e82198090b9fd986bbd98c2af6574d19f2ee37aL1-R25) * Updated function bodies and return statements to use consistent semicolon placement and code formatting throughout the affected files. [[1]](diffhunk://#diff-5de1b22e63fe3b6c9781c2a476db7440818f18d2aeb5c6c1ddeb446517cf001fL13-R15) [[2]](diffhunk://#diff-5de1b22e63fe3b6c9781c2a476db7440818f18d2aeb5c6c1ddeb446517cf001fL27-R35) [[3]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L17-R63) [[4]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L82-R82) [[5]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L158-R158) [[6]](diffhunk://#diff-540ce2405611334ce0bdff1f48d187218be99ce64fb92f054b9cf5a71cb1ed8cL13-R56) [[7]](diffhunk://#diff-540ce2405611334ce0bdff1f48d187218be99ce64fb92f054b9cf5a71cb1ed8cL72-R72) [[8]](diffhunk://#diff-d55dde4f28998eb9b30f332a1c96a4c79ec6a70b568bb51eea81d11a3715c35cL15-R26) [[9]](diffhunk://#diff-d55dde4f28998eb9b30f332a1c96a4c79ec6a70b568bb51eea81d11a3715c35cL39-R61) [[10]](diffhunk://#diff-183a38f7c78b7c2950c4bed87ff2843de146d960e28591865d91c3cd86c3fadbL51-R118) [[11]](diffhunk://#diff-183a38f7c78b7c2950c4bed87ff2843de146d960e28591865d91c3cd86c3fadbL188-R188) [[12]](diffhunk://#diff-0a98c2e661e58f226f98c90b2e82198090b9fd986bbd98c2af6574d19f2ee37aL1-R25) No business logic, UI, or feature behavior has been changed as part of this update. --- ## 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) ### 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. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- .../src/components/shared/DividerWithText.tsx | 12 +- .../components/shared/LoginRightCarousel.tsx | 66 +++++------ frontend/src/components/shared/loginSlides.ts | 4 +- frontend/src/routes/AuthCallback.tsx | 46 ++++---- frontend/src/routes/Landing.tsx | 34 +++--- frontend/src/routes/Login.tsx | 108 +++++++++--------- frontend/src/routes/Signup.tsx | 78 ++++++------- frontend/src/routes/authShared/AuthLayout.tsx | 56 ++++----- .../src/routes/login/EmailPasswordForm.tsx | 14 +-- frontend/src/routes/login/ErrorMessage.tsx | 4 +- frontend/src/routes/login/LoggedInState.tsx | 24 ++-- frontend/src/routes/login/LoginHeader.tsx | 2 +- frontend/src/routes/login/NavigationLink.tsx | 2 +- frontend/src/routes/login/OAuthButtons.tsx | 16 +-- frontend/src/routes/signup/AuthService.ts | 30 ++--- frontend/src/routes/signup/SignupForm.tsx | 18 +-- .../src/routes/signup/SignupFormValidation.ts | 32 +++--- .../src/tests/missingTranslations.test.ts | 2 +- 18 files changed, 274 insertions(+), 274 deletions(-) diff --git a/frontend/src/components/shared/DividerWithText.tsx b/frontend/src/components/shared/DividerWithText.tsx index 9b82240a1..86984969e 100644 --- a/frontend/src/components/shared/DividerWithText.tsx +++ b/frontend/src/components/shared/DividerWithText.tsx @@ -1,4 +1,4 @@ -import './dividerWithText/DividerWithText.css' +import './dividerWithText/DividerWithText.css'; interface TextDividerProps { text?: string @@ -10,9 +10,9 @@ interface TextDividerProps { } export default function DividerWithText({ text, className = '', style, variant = 'default', respondsToDarkMode = true, opacity }: TextDividerProps) { - const variantClass = variant === 'subcategory' ? 'subcategory' : '' - const themeClass = respondsToDarkMode ? '' : 'force-light' - const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style + const variantClass = variant === 'subcategory' ? 'subcategory' : ''; + const themeClass = respondsToDarkMode ? '' : 'force-light'; + const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style; if (text) { return ( @@ -24,7 +24,7 @@ export default function DividerWithText({ text, className = '', style, variant = {text}
- ) + ); } return ( @@ -32,5 +32,5 @@ export default function DividerWithText({ text, className = '', style, variant = className={`h-px my-2.5 ${themeClass} ${className}`} style={styleWithOpacity} /> - ) + ); } diff --git a/frontend/src/components/shared/LoginRightCarousel.tsx b/frontend/src/components/shared/LoginRightCarousel.tsx index c2ebb29bf..eb7e7ab73 100644 --- a/frontend/src/components/shared/LoginRightCarousel.tsx +++ b/frontend/src/components/shared/LoginRightCarousel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react'; import { BASE_PATH } from '../../constants/app'; type ImageSlide = { src: string; alt?: string; cornerModelUrl?: string; title?: string; subtitle?: string; followMouseTilt?: boolean; tiltMaxDeg?: number } @@ -14,53 +14,53 @@ export default function LoginRightCarousel({ initialSeconds?: number slideSeconds?: number }) { - const totalSlides = imageSlides.length - const [index, setIndex] = useState(0) - const mouse = useRef({ x: 0, y: 0 }) + const totalSlides = imageSlides.length; + const [index, setIndex] = useState(0); + const mouse = useRef({ x: 0, y: 0 }); const durationsMs = useMemo(() => { - if (imageSlides.length === 0) return [] - return imageSlides.map((_, i) => (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000) - }, [imageSlides, initialSeconds, slideSeconds]) + if (imageSlides.length === 0) return []; + return imageSlides.map((_, i) => (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000); + }, [imageSlides, initialSeconds, slideSeconds]); useEffect(() => { - if (totalSlides <= 1) return + if (totalSlides <= 1) return; const timeout = setTimeout(() => { - setIndex((i) => (i + 1) % totalSlides) - }, durationsMs[index] ?? slideSeconds * 1000) - return () => clearTimeout(timeout) - }, [index, totalSlides, durationsMs, slideSeconds]) + setIndex((i) => (i + 1) % totalSlides); + }, durationsMs[index] ?? slideSeconds * 1000); + return () => clearTimeout(timeout); + }, [index, totalSlides, durationsMs, slideSeconds]); useEffect(() => { const onMove = (e: MouseEvent) => { - mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1 - mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1 - } - window.addEventListener('mousemove', onMove) - return () => window.removeEventListener('mousemove', onMove) - }, []) + mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1; + }; + window.addEventListener('mousemove', onMove); + return () => window.removeEventListener('mousemove', onMove); + }, []); function TiltImage({ src, alt, enabled, maxDeg = 6 }: { src: string; alt?: string; enabled: boolean; maxDeg?: number }) { - const imgRef = useRef(null) + const imgRef = useRef(null); useEffect(() => { - const el = imgRef.current - if (!el) return + const el = imgRef.current; + if (!el) return; - let raf = 0 + let raf = 0; const tick = () => { if (enabled) { - const rotY = (mouse.current.x || 0) * maxDeg - const rotX = -(mouse.current.y || 0) * maxDeg - el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)` + const rotY = (mouse.current.x || 0) * maxDeg; + const rotX = -(mouse.current.y || 0) * maxDeg; + el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)`; } else { - el.style.transform = 'translateY(-2rem)' + el.style.transform = 'translateY(-2rem)'; } - raf = requestAnimationFrame(tick) - } - raf = requestAnimationFrame(tick) - return () => cancelAnimationFrame(raf) - }, [enabled, maxDeg]) + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [enabled, maxDeg]); return ( - ) + ); } return ( @@ -155,5 +155,5 @@ export default function LoginRightCarousel({ ))} - ) + ); } diff --git a/frontend/src/components/shared/loginSlides.ts b/frontend/src/components/shared/loginSlides.ts index aa3d7f443..690f84f6c 100644 --- a/frontend/src/components/shared/loginSlides.ts +++ b/frontend/src/components/shared/loginSlides.ts @@ -38,6 +38,6 @@ export const loginSlides: LoginCarouselSlide[] = [ followMouseTilt: true, tiltMaxDeg: 5, }, -] +]; -export default loginSlides +export default loginSlides; diff --git a/frontend/src/routes/AuthCallback.tsx b/frontend/src/routes/AuthCallback.tsx index 285d58251..be4cd5424 100644 --- a/frontend/src/routes/AuthCallback.tsx +++ b/frontend/src/routes/AuthCallback.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { useAuth } from '../auth/UseSession' +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth/UseSession'; /** * OAuth Callback Handler @@ -10,50 +10,50 @@ import { useAuth } from '../auth/UseSession' * We extract it, store in localStorage, and redirect to the home page. */ export default function AuthCallback() { - const navigate = useNavigate() - const { refreshSession } = useAuth() + const navigate = useNavigate(); + const { refreshSession } = useAuth(); useEffect(() => { const handleCallback = async () => { try { - console.log('[AuthCallback] Handling OAuth callback...') + console.log('[AuthCallback] Handling OAuth callback...'); // Extract JWT from URL fragment (#access_token=...) - const hash = window.location.hash.substring(1) // Remove '#' - const params = new URLSearchParams(hash) - const token = params.get('access_token') + const hash = window.location.hash.substring(1); // Remove '#' + const params = new URLSearchParams(hash); + const token = params.get('access_token'); if (!token) { - console.error('[AuthCallback] No access_token in URL fragment') + console.error('[AuthCallback] No access_token in URL fragment'); navigate('/login', { replace: true, state: { error: 'OAuth login failed - no token received.' } - }) - return + }); + return; } // Store JWT in localStorage - localStorage.setItem('stirling_jwt', token) - console.log('[AuthCallback] JWT stored in localStorage') + localStorage.setItem('stirling_jwt', token); + console.log('[AuthCallback] JWT stored in localStorage'); // Refresh session to load user info into state - await refreshSession() + await refreshSession(); - console.log('[AuthCallback] Session refreshed, redirecting to home') + console.log('[AuthCallback] Session refreshed, redirecting to home'); // Clear the hash from URL and redirect to home page - navigate('/', { replace: true }) + navigate('/', { replace: true }); } catch (error) { - console.error('[AuthCallback] Error:', error) + console.error('[AuthCallback] Error:', error); navigate('/login', { replace: true, state: { error: 'OAuth login failed. Please try again.' } - }) + }); } - } + }; - handleCallback() - }, [navigate, refreshSession]) + handleCallback(); + }, [navigate, refreshSession]); return (
- ) + ); } diff --git a/frontend/src/routes/Landing.tsx b/frontend/src/routes/Landing.tsx index 0eb2ba091..6f473f6f1 100644 --- a/frontend/src/routes/Landing.tsx +++ b/frontend/src/routes/Landing.tsx @@ -1,8 +1,8 @@ -import { Navigate, useLocation } from 'react-router-dom' -import { useAuth } from '../auth/UseSession' -import { useAppConfig } from '../hooks/useAppConfig' -import HomePage from '../pages/HomePage' -import Login from './Login' +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../auth/UseSession'; +import { useAppConfig } from '../hooks/useAppConfig'; +import HomePage from '../pages/HomePage'; +import Login from './Login'; /** * Landing component - Smart router based on authentication status @@ -12,18 +12,18 @@ import Login from './Login' * If user is not authenticated: Show Login or redirect to /login */ export default function Landing() { - const { session, loading: authLoading } = useAuth() - const { config, loading: configLoading } = useAppConfig() - const location = useLocation() + const { session, loading: authLoading } = useAuth(); + const { config, loading: configLoading } = useAppConfig(); + const location = useLocation(); - const loading = authLoading || configLoading + const loading = authLoading || configLoading; console.log('[Landing] State:', { pathname: location.pathname, loading, hasSession: !!session, loginEnabled: config?.enableLogin, - }) + }); // Show loading while checking auth and config if (loading) { @@ -36,27 +36,27 @@ export default function Landing() { - ) + ); } // If login is disabled, show app directly (anonymous mode) if (config?.enableLogin === false) { - console.debug('[Landing] Login disabled - showing app in anonymous mode') - return + console.debug('[Landing] Login disabled - showing app in anonymous mode'); + return ; } // If we have a session, show the main app if (session) { - return + return ; } // If we're at home route ("/"), show login directly (marketing/landing page) // Otherwise navigate to login (fixes URL mismatch for tool routes) - const isHome = location.pathname === '/' || location.pathname === '' + const isHome = location.pathname === '/' || location.pathname === ''; if (isHome) { - return + return ; } // For non-home routes without auth, navigate to login (preserves from location) - return + return ; } diff --git a/frontend/src/routes/Login.tsx b/frontend/src/routes/Login.tsx index 61efb0c74..04c345936 100644 --- a/frontend/src/routes/Login.tsx +++ b/frontend/src/routes/Login.tsx @@ -1,42 +1,42 @@ -import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { springAuth } from '../auth/springAuthClient' -import { useAuth } from '../auth/UseSession' -import { useTranslation } from 'react-i18next' -import { useDocumentMeta } from '../hooks/useDocumentMeta' -import AuthLayout from './authShared/AuthLayout' +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { springAuth } from '../auth/springAuthClient'; +import { useAuth } from '../auth/UseSession'; +import { useTranslation } from 'react-i18next'; +import { useDocumentMeta } from '../hooks/useDocumentMeta'; +import AuthLayout from './authShared/AuthLayout'; // Import login components -import LoginHeader from './login/LoginHeader' -import ErrorMessage from './login/ErrorMessage' -import EmailPasswordForm from './login/EmailPasswordForm' -import OAuthButtons from './login/OAuthButtons' -import DividerWithText from '../components/shared/DividerWithText' -import LoggedInState from './login/LoggedInState' -import { BASE_PATH } from '../constants/app' +import LoginHeader from './login/LoginHeader'; +import ErrorMessage from './login/ErrorMessage'; +import EmailPasswordForm from './login/EmailPasswordForm'; +import OAuthButtons from './login/OAuthButtons'; +import DividerWithText from '../components/shared/DividerWithText'; +import LoggedInState from './login/LoggedInState'; +import { BASE_PATH } from '../constants/app'; export default function Login() { - const navigate = useNavigate() - const { session, loading } = useAuth() - const { t } = useTranslation() - const [isSigningIn, setIsSigningIn] = useState(false) - const [error, setError] = useState(null) - const [showEmailForm, setShowEmailForm] = useState(false) - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') + const navigate = useNavigate(); + const { session, loading } = useAuth(); + const { t } = useTranslation(); + const [isSigningIn, setIsSigningIn] = useState(false); + const [error, setError] = useState(null); + const [showEmailForm, setShowEmailForm] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); // Prefill email from query param (e.g. after password reset) useEffect(() => { try { - const url = new URL(window.location.href) - const emailFromQuery = url.searchParams.get('email') + const url = new URL(window.location.href); + const emailFromQuery = url.searchParams.get('email'); if (emailFromQuery) { - setEmail(emailFromQuery) + setEmail(emailFromQuery); } } catch (_) { // ignore } - }, []) + }, []); const baseUrl = window.location.origin + BASE_PATH; @@ -48,74 +48,74 @@ export default function Login() { ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'), ogImage: `${baseUrl}/og_images/home.png`, ogUrl: `${window.location.origin}${window.location.pathname}` - }) + }); // Show logged in state if authenticated if (session && !loading) { - return + return ; } const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => { try { - setIsSigningIn(true) - setError(null) + setIsSigningIn(true); + setError(null); - console.log(`[Login] Signing in with ${provider}`) + console.log(`[Login] Signing in with ${provider}`); // Redirect to Spring OAuth2 endpoint const { error } = await springAuth.signInWithOAuth({ provider, options: { redirectTo: `${BASE_PATH}/auth/callback` } - }) + }); if (error) { - console.error(`[Login] ${provider} error:`, error) - setError(t('login.failedToSignIn', { provider, message: error.message }) || `Failed to sign in with ${provider}`) + console.error(`[Login] ${provider} error:`, error); + setError(t('login.failedToSignIn', { provider, message: error.message }) || `Failed to sign in with ${provider}`); } } catch (err) { - console.error(`[Login] Unexpected error:`, err) - setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred') + console.error(`[Login] Unexpected error:`, err); + setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred'); } finally { - setIsSigningIn(false) + setIsSigningIn(false); } - } + }; const signInWithEmail = async () => { if (!email || !password) { - setError(t('login.pleaseEnterBoth') || 'Please enter both email and password') - return + setError(t('login.pleaseEnterBoth') || 'Please enter both email and password'); + return; } try { - setIsSigningIn(true) - setError(null) + setIsSigningIn(true); + setError(null); - console.log('[Login] Signing in with email:', email) + console.log('[Login] Signing in with email:', email); const { user, session, error } = await springAuth.signInWithPassword({ email: email.trim(), password: password - }) + }); if (error) { - console.error('[Login] Email sign in error:', error) - setError(error.message) + console.error('[Login] Email sign in error:', error); + setError(error.message); } else if (user && session) { - console.log('[Login] Email sign in successful') + console.log('[Login] Email sign in successful'); // Auth state will update automatically and Landing will redirect to home // No need to navigate manually here } } catch (err) { - console.error('[Login] Unexpected error:', err) - setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred') + console.error('[Login] Unexpected error:', err); + setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred'); } finally { - setIsSigningIn(false) + setIsSigningIn(false); } - } + }; const handleForgotPassword = () => { - navigate('/auth/reset') - } + navigate('/auth/reset'); + }; return ( @@ -185,5 +185,5 @@ export default function Login() { - ) + ); } diff --git a/frontend/src/routes/Signup.tsx b/frontend/src/routes/Signup.tsx index 0a5ee32ac..465ff338b 100644 --- a/frontend/src/routes/Signup.tsx +++ b/frontend/src/routes/Signup.tsx @@ -1,28 +1,28 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { useTranslation } from 'react-i18next' -import { useDocumentMeta } from '../hooks/useDocumentMeta' -import AuthLayout from './authShared/AuthLayout' -import './authShared/auth.css' -import { BASE_PATH } from '../constants/app' +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useDocumentMeta } from '../hooks/useDocumentMeta'; +import AuthLayout from './authShared/AuthLayout'; +import './authShared/auth.css'; +import { BASE_PATH } from '../constants/app'; // Import signup components -import LoginHeader from './login/LoginHeader' -import ErrorMessage from './login/ErrorMessage' -import DividerWithText from '../components/shared/DividerWithText' -import SignupForm from './signup/SignupForm' -import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation' -import { useAuthService } from './signup/AuthService' +import LoginHeader from './login/LoginHeader'; +import ErrorMessage from './login/ErrorMessage'; +import DividerWithText from '../components/shared/DividerWithText'; +import SignupForm from './signup/SignupForm'; +import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation'; +import { useAuthService } from './signup/AuthService'; export default function Signup() { - const navigate = useNavigate() - const { t } = useTranslation() - const [isSigningUp, setIsSigningUp] = useState(false) - const [error, setError] = useState(null) - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [fieldErrors, setFieldErrors] = useState({}) + const navigate = useNavigate(); + const { t } = useTranslation(); + const [isSigningUp, setIsSigningUp] = useState(false); + const [error, setError] = useState(null); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [fieldErrors, setFieldErrors] = useState({}); const baseUrl = window.location.origin + BASE_PATH; @@ -34,38 +34,38 @@ export default function Signup() { ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'), ogImage: `${baseUrl}/og_images/home.png`, ogUrl: `${window.location.origin}${window.location.pathname}` - }) + }); - const { validateSignupForm } = useSignupFormValidation() - const { signUp } = useAuthService() + const { validateSignupForm } = useSignupFormValidation(); + const { signUp } = useAuthService(); const handleSignUp = async () => { - const validation = validateSignupForm(email, password, confirmPassword) + const validation = validateSignupForm(email, password, confirmPassword); if (!validation.isValid) { - setError(validation.error) - setFieldErrors(validation.fieldErrors || {}) - return + setError(validation.error); + setFieldErrors(validation.fieldErrors || {}); + return; } try { - setIsSigningUp(true) - setError(null) - setFieldErrors({}) + setIsSigningUp(true); + setError(null); + setFieldErrors({}); - const result = await signUp(email, password, '') + const result = await signUp(email, password, ''); if (result.user) { // Show success message and redirect to login - setError(null) - setTimeout(() => navigate('/login'), 2000) + setError(null); + setTimeout(() => navigate('/login'), 2000); } } catch (err) { - console.error('[Signup] Unexpected error:', err) - setError(err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' })) + console.error('[Signup] Unexpected error:', err); + setError(err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' })); } finally { - setIsSigningUp(false) + setIsSigningUp(false); } - } + }; return ( @@ -101,5 +101,5 @@ export default function Signup() { - ) + ); } diff --git a/frontend/src/routes/authShared/AuthLayout.tsx b/frontend/src/routes/authShared/AuthLayout.tsx index e32f629e0..99c4b69de 100644 --- a/frontend/src/routes/authShared/AuthLayout.tsx +++ b/frontend/src/routes/authShared/AuthLayout.tsx @@ -1,51 +1,51 @@ -import React, { useEffect, useRef, useState } from 'react' -import LoginRightCarousel from '../../components/shared/LoginRightCarousel' -import loginSlides from '../../components/shared/loginSlides' -import styles from './AuthLayout.module.css' +import React, { useEffect, useRef, useState } from 'react'; +import LoginRightCarousel from '../../components/shared/LoginRightCarousel'; +import loginSlides from '../../components/shared/loginSlides'; +import styles from './AuthLayout.module.css'; interface AuthLayoutProps { children: React.ReactNode } export default function AuthLayout({ children }: AuthLayoutProps) { - const cardRef = useRef(null) - const [hideRightPanel, setHideRightPanel] = useState(false) + const cardRef = useRef(null); + const [hideRightPanel, setHideRightPanel] = useState(false); // Force light mode on auth pages useEffect(() => { - const htmlElement = document.documentElement - const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme') + const htmlElement = document.documentElement; + const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme'); // Set light mode - htmlElement.setAttribute('data-mantine-color-scheme', 'light') + htmlElement.setAttribute('data-mantine-color-scheme', 'light'); // Cleanup: restore previous theme when leaving auth pages return () => { if (previousColorScheme) { - htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme) + htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme); } - } - }, []) + }; + }, []); useEffect(() => { const update = () => { // Use viewport to avoid hysteresis when the card is already in single-column mode - const viewportWidth = window.innerWidth - const viewportHeight = window.innerHeight - const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96) // matches min(73.75rem, 96vw) - const columnWidth = cardWidthIfTwoCols / 2 - const tooNarrow = columnWidth < 470 - const tooShort = viewportHeight < 740 - setHideRightPanel(tooNarrow || tooShort) - } - update() - window.addEventListener('resize', update) - window.addEventListener('orientationchange', update) + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96); // matches min(73.75rem, 96vw) + const columnWidth = cardWidthIfTwoCols / 2; + const tooNarrow = columnWidth < 470; + const tooShort = viewportHeight < 740; + setHideRightPanel(tooNarrow || tooShort); + }; + update(); + window.addEventListener('resize', update); + window.addEventListener('orientationchange', update); return () => { - window.removeEventListener('resize', update) - window.removeEventListener('orientationchange', update) - } - }, []) + window.removeEventListener('resize', update); + window.removeEventListener('orientationchange', update); + }; + }, []); return (
@@ -64,5 +64,5 @@ export default function AuthLayout({ children }: AuthLayoutProps) { )}
- ) + ); } diff --git a/frontend/src/routes/login/EmailPasswordForm.tsx b/frontend/src/routes/login/EmailPasswordForm.tsx index 7036e24cf..dff38026f 100644 --- a/frontend/src/routes/login/EmailPasswordForm.tsx +++ b/frontend/src/routes/login/EmailPasswordForm.tsx @@ -1,5 +1,5 @@ -import { useTranslation } from 'react-i18next' -import '../authShared/auth.css' +import { useTranslation } from 'react-i18next'; +import '../authShared/auth.css'; interface EmailPasswordFormProps { email: string @@ -27,12 +27,12 @@ export default function EmailPasswordForm({ showPasswordField = true, fieldErrors = {} }: EmailPasswordFormProps) { - const { t } = useTranslation() + const { t } = useTranslation(); const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - onSubmit() - } + e.preventDefault(); + onSubmit(); + }; return (
@@ -82,5 +82,5 @@ export default function EmailPasswordForm({ {submitButtonText}
- ) + ); } diff --git a/frontend/src/routes/login/ErrorMessage.tsx b/frontend/src/routes/login/ErrorMessage.tsx index 4f237b77c..4563a2450 100644 --- a/frontend/src/routes/login/ErrorMessage.tsx +++ b/frontend/src/routes/login/ErrorMessage.tsx @@ -3,11 +3,11 @@ interface ErrorMessageProps { } export default function ErrorMessage({ error }: ErrorMessageProps) { - if (!error) return null + if (!error) return null; return (

{error}

- ) + ); } diff --git a/frontend/src/routes/login/LoggedInState.tsx b/frontend/src/routes/login/LoggedInState.tsx index 19483b8bc..7caf92bcf 100644 --- a/frontend/src/routes/login/LoggedInState.tsx +++ b/frontend/src/routes/login/LoggedInState.tsx @@ -1,20 +1,20 @@ -import { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { useAuth } from '../../auth/UseSession' -import { useTranslation } from 'react-i18next' +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../auth/UseSession'; +import { useTranslation } from 'react-i18next'; export default function LoggedInState() { - const navigate = useNavigate() - const { user } = useAuth() - const { t } = useTranslation() + const navigate = useNavigate(); + const { user } = useAuth(); + const { t } = useTranslation(); useEffect(() => { const timer = setTimeout(() => { - navigate('/') - }, 2000) + navigate('/'); + }, 2000); - return () => clearTimeout(timer) - }, [navigate]) + return () => clearTimeout(timer); + }, [navigate]); return (
- ) + ); } diff --git a/frontend/src/routes/login/LoginHeader.tsx b/frontend/src/routes/login/LoginHeader.tsx index 093100574..dde6867ab 100644 --- a/frontend/src/routes/login/LoginHeader.tsx +++ b/frontend/src/routes/login/LoginHeader.tsx @@ -18,5 +18,5 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {

{subtitle}

)} - ) + ); } diff --git a/frontend/src/routes/login/NavigationLink.tsx b/frontend/src/routes/login/NavigationLink.tsx index 965a659b9..935654381 100644 --- a/frontend/src/routes/login/NavigationLink.tsx +++ b/frontend/src/routes/login/NavigationLink.tsx @@ -15,5 +15,5 @@ export default function NavigationLink({ onClick, text, isDisabled = false }: Na {text} - ) + ); } diff --git a/frontend/src/routes/login/OAuthButtons.tsx b/frontend/src/routes/login/OAuthButtons.tsx index 0de8f3179..b5d5ec54c 100644 --- a/frontend/src/routes/login/OAuthButtons.tsx +++ b/frontend/src/routes/login/OAuthButtons.tsx @@ -1,5 +1,5 @@ -import { useTranslation } from 'react-i18next' -import { BASE_PATH } from '../../constants/app' +import { useTranslation } from 'react-i18next'; +import { BASE_PATH } from '../../constants/app'; // OAuth provider configuration const oauthProviders = [ @@ -7,7 +7,7 @@ const oauthProviders = [ { id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false }, { id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true }, { id: 'azure', label: 'Microsoft', file: 'microsoft.svg', isDisabled: true } -] +]; interface OAuthButtonsProps { onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void @@ -16,10 +16,10 @@ interface OAuthButtonsProps { } export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical' }: OAuthButtonsProps) { - const { t } = useTranslation() + const { t } = useTranslation(); // Filter out disabled providers - don't show them at all - const enabledProviders = oauthProviders.filter(p => !p.isDisabled) + const enabledProviders = oauthProviders.filter(p => !p.isDisabled); if (layout === 'icons') { return ( @@ -37,7 +37,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' ))} - ) + ); } if (layout === 'grid') { @@ -56,7 +56,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' ))} - ) + ); } return ( @@ -74,5 +74,5 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' ))} - ) + ); } diff --git a/frontend/src/routes/signup/AuthService.ts b/frontend/src/routes/signup/AuthService.ts index ce2426fa2..a68e72d24 100644 --- a/frontend/src/routes/signup/AuthService.ts +++ b/frontend/src/routes/signup/AuthService.ts @@ -1,5 +1,5 @@ -import { springAuth } from '../../auth/springAuthClient' -import { BASE_PATH } from '../../constants/app' +import { springAuth } from '../../auth/springAuthClient'; +import { BASE_PATH } from '../../constants/app'; export const useAuthService = () => { @@ -8,7 +8,7 @@ export const useAuthService = () => { password: string, name: string ) => { - console.log('[Signup] Creating account for:', email) + console.log('[Signup] Creating account for:', email); const { user, session, error } = await springAuth.signUp({ email: email.trim(), @@ -17,38 +17,38 @@ export const useAuthService = () => { data: { full_name: name }, emailRedirectTo: `${BASE_PATH}/auth/callback` } - }) + }); if (error) { - console.error('[Signup] Sign up error:', error) - throw new Error(error.message) + console.error('[Signup] Sign up error:', error); + throw new Error(error.message); } if (user) { - console.log('[Signup] Sign up successful:', user) + console.log('[Signup] Sign up successful:', user); return { user: user, session: session, requiresEmailConfirmation: user && !session - } + }; } - throw new Error('Unknown error occurred during signup') - } + throw new Error('Unknown error occurred during signup'); + }; const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => { const { error } = await springAuth.signInWithOAuth({ provider, options: { redirectTo: `${BASE_PATH}/auth/callback` } - }) + }); if (error) { - throw new Error(error.message) + throw new Error(error.message); } - } + }; return { signUp, signInWithProvider - } -} \ No newline at end of file + }; +}; \ No newline at end of file diff --git a/frontend/src/routes/signup/SignupForm.tsx b/frontend/src/routes/signup/SignupForm.tsx index 2ae809467..9ff7bc7f4 100644 --- a/frontend/src/routes/signup/SignupForm.tsx +++ b/frontend/src/routes/signup/SignupForm.tsx @@ -1,7 +1,7 @@ -import { useEffect } from 'react' -import '../authShared/auth.css' -import { useTranslation } from 'react-i18next' -import { SignupFieldErrors } from './SignupFormValidation' +import { useEffect } from 'react'; +import '../authShared/auth.css'; +import { useTranslation } from 'react-i18next'; +import { SignupFieldErrors } from './SignupFormValidation'; interface SignupFormProps { name?: string @@ -38,14 +38,14 @@ export default function SignupForm({ showName = false, showTerms = false }: SignupFormProps) { - const { t } = useTranslation() - const showConfirm = password.length >= 4 + const { t } = useTranslation(); + const showConfirm = password.length >= 4; useEffect(() => { if (!showConfirm && confirmPassword) { - setConfirmPassword('') + setConfirmPassword(''); } - }, [showConfirm, confirmPassword, setConfirmPassword]) + }, [showConfirm, confirmPassword, setConfirmPassword]); return ( <> @@ -158,5 +158,5 @@ export default function SignupForm({ {isSubmitting ? t('signup.creatingAccount') : t('signup.signUp')} - ) + ); } \ No newline at end of file diff --git a/frontend/src/routes/signup/SignupFormValidation.ts b/frontend/src/routes/signup/SignupFormValidation.ts index 7abe498a0..40c1ad7e7 100644 --- a/frontend/src/routes/signup/SignupFormValidation.ts +++ b/frontend/src/routes/signup/SignupFormValidation.ts @@ -1,4 +1,4 @@ -import { useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next'; export interface SignupFieldErrors { name?: string @@ -14,7 +14,7 @@ export interface SignupValidationResult { } export const useSignupFormValidation = () => { - const { t } = useTranslation() + const { t } = useTranslation(); const validateSignupForm = ( email: string, @@ -22,45 +22,45 @@ export const useSignupFormValidation = () => { confirmPassword: string, name?: string ): SignupValidationResult => { - const fieldErrors: SignupFieldErrors = {} + const fieldErrors: SignupFieldErrors = {}; // Validate name if (name !== undefined && name !== null && !name.trim()) { - fieldErrors.name = t('signup.nameRequired', 'Name is required') + fieldErrors.name = t('signup.nameRequired', 'Name is required'); } // Validate email - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email) { - fieldErrors.email = t('signup.emailRequired', 'Email is required') + fieldErrors.email = t('signup.emailRequired', 'Email is required'); } else if (!emailRegex.test(email)) { - fieldErrors.email = t('signup.invalidEmail') + fieldErrors.email = t('signup.invalidEmail'); } // Validate password if (!password) { - fieldErrors.password = t('signup.passwordRequired', 'Password is required') + fieldErrors.password = t('signup.passwordRequired', 'Password is required'); } else if (password.length < 6) { - fieldErrors.password = t('signup.passwordTooShort') + fieldErrors.password = t('signup.passwordTooShort'); } // Validate confirm password if (!confirmPassword) { - fieldErrors.confirmPassword = t('signup.confirmPasswordRequired', 'Please confirm your password') + fieldErrors.confirmPassword = t('signup.confirmPasswordRequired', 'Please confirm your password'); } else if (password !== confirmPassword) { - fieldErrors.confirmPassword = t('signup.passwordsDoNotMatch') + fieldErrors.confirmPassword = t('signup.passwordsDoNotMatch'); } - const hasErrors = Object.keys(fieldErrors).length > 0 + const hasErrors = Object.keys(fieldErrors).length > 0; return { isValid: !hasErrors, error: null, // Don't show generic error, field errors are more specific fieldErrors: hasErrors ? fieldErrors : undefined - } - } + }; + }; return { validateSignupForm - } -} \ No newline at end of file + }; +}; \ No newline at end of file diff --git a/frontend/src/tests/missingTranslations.test.ts b/frontend/src/tests/missingTranslations.test.ts index 908ab1504..53d635e16 100644 --- a/frontend/src/tests/missingTranslations.test.ts +++ b/frontend/src/tests/missingTranslations.test.ts @@ -3,7 +3,7 @@ import path from 'path'; import ts from 'typescript'; import { describe, expect, test } from 'vitest'; -const REPO_ROOT = path.join(__dirname, '../../../') +const REPO_ROOT = path.join(__dirname, '../../../'); const SRC_ROOT = path.join(__dirname, '..'); const EN_GB_FILE = path.join(__dirname, '../../public/locales/en-GB/translation.json'); From 12d7165f83029b1ce822dd861cbe57087412bc88 Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 27 Oct 2025 17:26:08 +0100 Subject: [PATCH 4/9] fix: add missing id and name attributes to form input (#4589) # Description of Changes - Updated `TextInput` component to require `id` and `name` props. - Passed `id` and `name` down to the underlying `` element. - Updated `ToolSearch` component to provide explicit `id` and `name` for the search input. - This ensures form fields have unique identifiers, improving accessibility, browser autofill support, and form handling. --- ## 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) ### 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. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- frontend/src/components/shared/TextInput.tsx | 8 ++++++++ .../tools/toolPicker/ToolSearch.tsx | 20 ++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx index 63c69bb08..80c26dfdd 100644 --- a/frontend/src/components/shared/TextInput.tsx +++ b/frontend/src/components/shared/TextInput.tsx @@ -7,6 +7,10 @@ import styles from './textInput/TextInput.module.css'; * Props for the TextInput component */ export interface TextInputProps { + /** The input ID (required) */ + id: string; + /** The input name (required) */ + name: string; /** The input value (required) */ value: string; /** Callback when input value changes (required) */ @@ -36,6 +40,8 @@ export interface TextInputProps { } export const TextInput = forwardRef(({ + id, + name, value, onChange, placeholder, @@ -76,6 +82,8 @@ export const TextInput = forwardRef(({ onChange(e.currentTarget.value)} diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index d94003d2c..e4e2f416b 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -82,15 +82,17 @@ const ToolSearch = ({ }, [autoFocus]); const searchInput = ( - } - autoComplete="off" - onFocus={onFocus} - /> + } + autoComplete="off" + onFocus={onFocus} + /> ); if (mode === "filter") { From 20600ac1c33e1d2bae9f89eb4ce512fcf4f76fe7 Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 27 Oct 2025 17:32:45 +0100 Subject: [PATCH 5/9] ci(deps): pin GitHub Actions to SHAs, upgrade to checkout v5 & setup-* v5/6, adopt Node 22, harden runner, bump docker-compose (#4591) # Description of Changes --- ## 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) ### 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. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 65 ++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b38abe5dc..1c86df9bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,10 +31,15 @@ jobs: project: ${{ steps.changes.outputs.project }} openapi: ${{ steps.changes.outputs.openapi }} steps: - - uses: actions/checkout@v4.3.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Check for file changes - uses: dorny/paths-filter@v3.0.2 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: changes with: filters: .github/config/.files.yaml @@ -51,19 +56,19 @@ jobs: spring-security: [true, false] steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK ${{ matrix.jdk-version }} - uses: actions/setup-java@v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: ${{ matrix.jdk-version }} distribution: "temurin" - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4.4.2 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 - name: Build with Gradle and spring security ${{ matrix.spring-security }} @@ -89,7 +94,7 @@ jobs: done - name: Upload Test Reports if: always() - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }} path: | @@ -106,26 +111,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 17 - uses: actions/setup-java@v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" - - uses: gradle/actions/setup-gradle@v4.4.2 + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 - name: Generate OpenAPI documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs env: DISABLE_ADDITIONAL_FEATURES: true - name: Upload OpenAPI Documentation - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: openapi-docs path: ./SwaggerDoc.json @@ -134,15 +139,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@v2.12.2 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Node.js - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: '20' + node-version: '22' cache: 'npm' cache-dependency-path: frontend/package-lock.json - name: Install frontend dependencies @@ -154,7 +159,7 @@ jobs: - name: Run frontend tests run: cd frontend && npm run test -- --run - name: Upload frontend build artifacts - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: frontend-build path: frontend/dist/ @@ -166,13 +171,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 17 - uses: actions/setup-java@v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" @@ -180,7 +185,7 @@ jobs: run: ./gradlew clean checkLicense - name: FAILED - check the licenses for compatibility if: failure() - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dependencies-without-allowed-license.json path: build/reports/dependency-license/dependencies-without-allowed-license.json @@ -207,15 +212,15 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout Repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Java 17 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" @@ -225,11 +230,11 @@ jobs: - name: Install Docker Compose run: | - sudo curl -SL "https://github.com/docker/compose/releases/download/v2.37.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.12" cache: 'pip' # caching pip dependencies @@ -256,21 +261,21 @@ jobs: docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"] steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout Repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 17 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" - name: Set up Gradle - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 From c33d705c32c519597c15c0c042c9c2b53948b130 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:37:13 +0000 Subject: [PATCH 6/9] update terms (#4642) # Description of Changes --- ## 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) ### 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. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- app/core/src/main/resources/settings.yml.template | 4 ++-- frontend/src/components/shared/Footer.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 412b1abfc..1e577a368 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -106,8 +106,8 @@ mail: from: '' # sender email address legal: - termsAndConditions: https://www.stirlingpdf.com/terms # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder - privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder + termsAndConditions: https://www.stirling.com/legal/terms-of-service # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder + privacyPolicy: https://www.stirling.com/legal/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder diff --git a/frontend/src/components/shared/Footer.tsx b/frontend/src/components/shared/Footer.tsx index 0c94efa0e..68a42a5e5 100644 --- a/frontend/src/components/shared/Footer.tsx +++ b/frontend/src/components/shared/Footer.tsx @@ -12,8 +12,8 @@ interface FooterProps { } export default function Footer({ - privacyPolicy = '/privacy', - termsAndConditions = '/terms', + privacyPolicy = 'https://www.stirling.com/legal/privacy-policy', + termsAndConditions = 'https://www.stirling.com/legal/terms-of-service', accessibilityStatement = 'accessibility', analyticsEnabled = false }: FooterProps) { From 81dec53488183bd6514aef3217532d51392d1c5b Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 27 Oct 2025 17:40:43 +0100 Subject: [PATCH 7/9] style(frontend): remove redundant React imports in TypeScript components (#4738) # Description of Changes This pull request removes unnecessary imports of the default React object from multiple frontend files. The changes help clean up the codebase by only importing specific React hooks where needed, rather than importing the entire React object. **Code cleanup and import optimization:** * Removed default `React` imports from component files, retaining only the necessary React hooks (such as `useEffect`, `useState`, `useMemo`, etc.) in files like `FileEditor.tsx`, `FullscreenToolList.tsx`, `ToolPanel.tsx`, `PageNumberPreview.tsx`, `AdjustContrastPreview.tsx`, `AutomationRun.tsx`, `LocalEmbedPDFWithAnnotations.tsx`, `ToolRegistryProvider.tsx`, `useTranslatedToolRegistry.tsx`, and `AdjustContrast.tsx`. [[1]](diffhunk://#diff-481d0a2d8a1714d34d21181db63a020b08dfccfbfa80bf47ac9af382dff25310L1-R1) [[2]](diffhunk://#diff-1d6e9507cb0744e03ec0e80c510874bfc5054986b0275ae3b8592eb67b5ec0f2L1-R1) [[3]](diffhunk://#diff-8ee3da71652291722dc6130f44565c098fe0f9cdf5e8ec0ba3c631be8980b13eL1-R1) [[4]](diffhunk://#diff-ecc12bf9b557e947ae2f1866d07446b19bad1fbdf143bf231dd3076b1e794826L1-R1) [[5]](diffhunk://#diff-6ba4ca6f491368b62e160639e97207f5c1d35fee77f4eebd39133630e0ecb7a1L1-R1) [[6]](diffhunk://#diff-ff7cba3dba3b1f4ec4c8758a9fbe539351f96225284d0c61cca2642ec7a8e486L1-R1) [[7]](diffhunk://#diff-d99cf54aa50d266c08844fac31c79e73a7f1714adeedb186d1decab8b9fb7f78L1-R1) [[8]](diffhunk://#diff-3467ae2b00d2ea95c360bc367adfbae124a4fb1d2627e889d12fb00e288bf508L1-R1) [[9]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969L1-R1) [[10]](diffhunk://#diff-2fed64bea41254c30dcc038f6b92943272bcaa771af200b8a3dc1a2cef6b5ca7L2-R2) * Removed default `React` imports from presentational and settings components that do not use JSX at the top level, such as `Workbench.tsx`, `SliderWithInput.tsx`, `AdjustContrastBasicSettings.tsx`, `AdjustContrastColorSettings.tsx`, `AdjustContrastSingleStepSettings.tsx`, `FileSummaryHeader.tsx`, `SignatureSection.tsx`, `SignatureStatusBadge.tsx`, and `ThumbnailPreview.tsx`. [[1]](diffhunk://#diff-6ffa9f7048b8e2a454ccf52b712179784cf32d42ecac9c85331c595a4cee39b4L1) [[2]](diffhunk://#diff-a1159e58f6668bc6de9595b4014fd7b8e0a19f9efa75294ba80184cfe54b601fL1) [[3]](diffhunk://#diff-ce5bbd748c15bc456e7f01180b7ff04c80c782e3d6662384f28e032af36ed3ccL1) [[4]](diffhunk://#diff-494006ec5e237eb7b3a16b9bc144a6ed49ed38c547d95b68a89f69a5af6676ceL1) [[5]](diffhunk://#diff-e61a3e2d98c9601eea868062258b925e6f6d672f49df14e3684b12f736622db4L1) [[6]](diffhunk://#diff-97df8b451114e347bb3f581ff5c91057601fb821e224479e1106493ce9479dcdL1) [[7]](diffhunk://#diff-cc070bfc4dc892a4e9a2be725c9f27ab66bdbc821a525fad10e14b27096d4e5aL1) [[8]](diffhunk://#diff-c179df2634412e4938bcd686f86b3bdbd1a6039d8a8b62c44fd0c085cc58af74L1) [[9]](diffhunk://#diff-64403230a8c8e90135bd8d7cd275c40d8e22bd3a22ed642dec5451018eec3c10L1) These changes reduce unnecessary imports and make the codebase cleaner and more consistent. --- ## 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) ### 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. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- frontend/src/components/fileEditor/FileEditor.tsx | 2 +- frontend/src/components/layout/Workbench.tsx | 1 - .../components/shared/sliderWithInput/SliderWithInput.tsx | 1 - frontend/src/components/tools/FullscreenToolList.tsx | 6 +++--- frontend/src/components/tools/ToolPanel.tsx | 2 +- .../components/tools/addPageNumbers/PageNumberPreview.tsx | 2 +- .../tools/adjustContrast/AdjustContrastBasicSettings.tsx | 1 - .../tools/adjustContrast/AdjustContrastColorSettings.tsx | 1 - .../tools/adjustContrast/AdjustContrastPreview.tsx | 2 +- .../adjustContrast/AdjustContrastSingleStepSettings.tsx | 1 - frontend/src/components/tools/automate/AutomationRun.tsx | 2 +- .../validateSignature/reportView/FileSummaryHeader.tsx | 1 - .../tools/validateSignature/reportView/SignatureSection.tsx | 1 - .../validateSignature/reportView/SignatureStatusBadge.tsx | 1 - .../tools/validateSignature/reportView/ThumbnailPreview.tsx | 1 - .../src/components/viewer/LocalEmbedPDFWithAnnotations.tsx | 4 ++-- frontend/src/contexts/ToolRegistryProvider.tsx | 2 +- frontend/src/data/useTranslatedToolRegistry.tsx | 2 +- frontend/src/tools/AdjustContrast.tsx | 2 +- 19 files changed, 13 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 54901902c..8d894389e 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; +import { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { Text, Center, Box, LoadingOverlay, Stack } from '@mantine/core'; diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 74356b97d..4ec7d8017 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Box } from '@mantine/core'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; diff --git a/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx b/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx index e8e504ded..7a91cdbe3 100644 --- a/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx +++ b/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Slider, Text, Group, NumberInput } from '@mantine/core'; interface Props { diff --git a/frontend/src/components/tools/FullscreenToolList.tsx b/frontend/src/components/tools/FullscreenToolList.tsx index ce76b7375..686fdf182 100644 --- a/frontend/src/components/tools/FullscreenToolList.tsx +++ b/frontend/src/components/tools/FullscreenToolList.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { ToolRegistryEntry, getSubcategoryLabel, getSubcategoryColor, getSubcategoryIcon } from '../../data/toolsTaxonomy'; @@ -104,7 +104,7 @@ const FullscreenToolList = ({ {showRecentFavorites && ( <> {favoriteToolItems.length > 0 && ( -
0 && ( -
); -} \ No newline at end of file +} diff --git a/frontend/src/contexts/ToolRegistryProvider.tsx b/frontend/src/contexts/ToolRegistryProvider.tsx index 30a020501..e7dc7f53b 100644 --- a/frontend/src/contexts/ToolRegistryProvider.tsx +++ b/frontend/src/contexts/ToolRegistryProvider.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import type { ToolId } from '../types/toolId'; import type { ToolRegistry } from '../data/toolsTaxonomy'; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index f50b5ed55..f3b22dcc4 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import { useMemo } from "react"; import LocalIcon from "../components/shared/LocalIcon"; import { useTranslation } from "react-i18next"; import SplitPdfPanel from "../tools/Split"; diff --git a/frontend/src/tools/AdjustContrast.tsx b/frontend/src/tools/AdjustContrast.tsx index 0de528a40..7e92c0541 100644 --- a/frontend/src/tools/AdjustContrast.tsx +++ b/frontend/src/tools/AdjustContrast.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { createToolFlow } from '../components/tools/shared/createToolFlow'; import { BaseToolProps, ToolComponent } from '../types/tool'; import { useBaseTool } from '../hooks/tools/shared/useBaseTool'; From 2e932f30bfd7c95ef3ca1e100cc6337e60acdb20 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:45:18 +0000 Subject: [PATCH 8/9] docker (#4711) # Description of Changes --- ## 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) ### 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. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- docker/Dockerfile.unified | 141 ++++++ .../docker-compose-unified-backend.yml | 58 +++ .../compose/docker-compose-unified-both.yml | 59 +++ .../docker-compose-unified-frontend.yml | 63 +++ docker/unified/README.md | 458 ++++++++++++++++++ docker/unified/build.sh | 38 ++ docker/unified/entrypoint.sh | 176 +++++++ docker/unified/nginx.conf | 118 +++++ 8 files changed, 1111 insertions(+) create mode 100644 docker/Dockerfile.unified create mode 100644 docker/compose/docker-compose-unified-backend.yml create mode 100644 docker/compose/docker-compose-unified-both.yml create mode 100644 docker/compose/docker-compose-unified-frontend.yml create mode 100644 docker/unified/README.md create mode 100644 docker/unified/build.sh create mode 100644 docker/unified/entrypoint.sh create mode 100644 docker/unified/nginx.conf diff --git a/docker/Dockerfile.unified b/docker/Dockerfile.unified new file mode 100644 index 000000000..0f1c31cf8 --- /dev/null +++ b/docker/Dockerfile.unified @@ -0,0 +1,141 @@ +# Unified Dockerfile - Frontend + Backend in single container +# Supports MODE parameter: BOTH (default), FRONTEND, BACKEND + +# Stage 1: Build Frontend +FROM node:20-alpine AS frontend-build + +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +COPY frontend . +RUN npm run build + +# Stage 2: Build Backend +FROM gradle:8.14-jdk21 AS backend-build + +COPY build.gradle . +COPY settings.gradle . +COPY gradlew . +COPY gradle gradle/ +COPY app/core/build.gradle core/. +COPY app/common/build.gradle common/. +COPY app/proprietary/build.gradle proprietary/. +RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 + +WORKDIR /app +COPY . . + +RUN DISABLE_ADDITIONAL_FEATURES=false \ + STIRLING_PDF_DESKTOP_UI=false \ + ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube + +# Stage 3: Final unified image +FROM alpine:3.22.1 + +ARG VERSION_TAG + +# Labels +LABEL org.opencontainers.image.title="Stirling-PDF Unified" +LABEL org.opencontainers.image.description="Unified container for Stirling-PDF - Frontend + Backend with MODE parameter" +LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.vendor="Stirling-Tools" +LABEL org.opencontainers.image.url="https://www.stirlingpdf.com" +LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com" +LABEL maintainer="Stirling-Tools" +LABEL org.opencontainers.image.authors="Stirling-Tools" +LABEL org.opencontainers.image.version="${VERSION_TAG}" +LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, API, Spring Boot, React" + +# Copy backend files +COPY scripts /scripts +COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ +COPY --from=backend-build /app/app/core/build/libs/*.jar app.jar + +# Copy frontend files +COPY --from=frontend-build /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY docker/unified/nginx.conf /etc/nginx/nginx.conf +COPY docker/unified/entrypoint.sh /entrypoint.sh + +# Environment Variables +ENV DISABLE_ADDITIONAL_FEATURES=false \ + VERSION_TAG=$VERSION_TAG \ + JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ + JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ + UNO_PATH=/usr/lib/libreoffice/program \ + URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ + PATH=$PATH:/opt/venv/bin \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + MODE=BOTH \ + BACKEND_INTERNAL_PORT=8081 \ + VITE_API_BASE_URL=http://localhost:8080 + +# Install all dependencies +RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ + echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ + echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ + apk upgrade --no-cache -a && \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + tini \ + bash \ + curl \ + shadow \ + su-exec \ + openssl \ + openssl-dev \ + openjdk21-jre \ + nginx \ + # Doc conversion + gcompat \ + libc6-compat \ + libreoffice \ + # pdftohtml + poppler-utils \ + # OCR MY PDF + unpaper \ + tesseract-ocr-data-eng \ + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ + ocrmypdf \ + # CV + py3-opencv \ + python3 \ + py3-pip \ + py3-pillow@testing \ + py3-pdf2image@testing && \ + python3 -m venv /opt/venv && \ + /opt/venv/bin/pip install --upgrade pip setuptools && \ + /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ + ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ + mv /usr/share/tessdata /usr/share/tessdata-original && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \ + mkdir -p /var/lib/nginx/tmp /var/log/nginx && \ + fc-cache -f -v && \ + chmod +x /scripts/* && \ + chmod +x /entrypoint.sh && \ + # User permissions + addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx && \ + chown stirlingpdfuser:stirlingpdfgroup /app.jar + +EXPOSE 8080/tcp + +ENTRYPOINT ["tini", "--", "/entrypoint.sh"] diff --git a/docker/compose/docker-compose-unified-backend.yml b/docker/compose/docker-compose-unified-backend.yml new file mode 100644 index 000000000..b8ebfd42b --- /dev/null +++ b/docker/compose/docker-compose-unified-backend.yml @@ -0,0 +1,58 @@ +# Example Docker Compose for Unified Stirling-PDF Container +# MODE=BACKEND: Backend API only (no frontend) + +services: + stirling-pdf-backend-only: + container_name: Stirling-PDF-Backend-Only + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8080:8080" + volumes: + - ./stirling/data:/usr/share/tessdata:rw + - ./stirling/config:/configs:rw + - ./stirling/logs:/logs:rw + - ./stirling/customFiles:/customFiles:rw + - ./stirling/pipeline:/pipeline:rw + environment: + # MODE parameter: BACKEND only + MODE: BACKEND + + # Standard Stirling-PDF configuration + DISABLE_ADDITIONAL_FEATURES: "false" + DOCKER_ENABLE_SECURITY: "false" + PUID: 1000 + PGID: 1000 + UMASK: "022" + + # Application settings + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + + # Optional: Add OCR languages (comma-separated) + # TESSERACT_LANGS: "deu,fra,spa" + + # Optional: Java memory settings + # JAVA_CUSTOM_OPTS: "-Xmx4g" + + restart: unless-stopped + + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + deploy: + resources: + limits: + memory: 4G + reservations: + memory: 2G + +# Access the API at: http://localhost:8080/api +# Swagger UI at: http://localhost:8080/swagger-ui/index.html diff --git a/docker/compose/docker-compose-unified-both.yml b/docker/compose/docker-compose-unified-both.yml new file mode 100644 index 000000000..92e08e4aa --- /dev/null +++ b/docker/compose/docker-compose-unified-both.yml @@ -0,0 +1,59 @@ +# Example Docker Compose for Unified Stirling-PDF Container +# MODE=BOTH (default): Frontend + Backend in single container on port 8080 + +services: + stirling-pdf-unified: + container_name: Stirling-PDF-Unified-Both + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8080:8080" + volumes: + - ./stirling/data:/usr/share/tessdata:rw + - ./stirling/config:/configs:rw + - ./stirling/logs:/logs:rw + - ./stirling/customFiles:/customFiles:rw + - ./stirling/pipeline:/pipeline:rw + environment: + # MODE parameter: BOTH (default), FRONTEND, or BACKEND + MODE: BOTH + + # Backend runs internally on this port when MODE=BOTH + BACKEND_INTERNAL_PORT: 8081 + + # Standard Stirling-PDF configuration + DISABLE_ADDITIONAL_FEATURES: "false" + DOCKER_ENABLE_SECURITY: "false" + PUID: 1000 + PGID: 1000 + UMASK: "022" + + # Application settings + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Your locally hosted one-stop-shop for all your PDF needs + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + + # Optional: Add OCR languages (comma-separated) + # TESSERACT_LANGS: "deu,fra,spa" + + # Optional: Java memory settings + # JAVA_CUSTOM_OPTS: "-Xmx4g" + + restart: unless-stopped + + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + deploy: + resources: + limits: + memory: 4G + reservations: + memory: 2G diff --git a/docker/compose/docker-compose-unified-frontend.yml b/docker/compose/docker-compose-unified-frontend.yml new file mode 100644 index 000000000..c7d217b34 --- /dev/null +++ b/docker/compose/docker-compose-unified-frontend.yml @@ -0,0 +1,63 @@ +# Example Docker Compose for Unified Stirling-PDF Container +# MODE=FRONTEND: Frontend only, connects to separate backend + +services: + stirling-pdf-backend: + container_name: Stirling-PDF-Backend + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8081:8080" + volumes: + - ./stirling/data:/usr/share/tessdata:rw + - ./stirling/config:/configs:rw + - ./stirling/logs:/logs:rw + - ./stirling/customFiles:/customFiles:rw + - ./stirling/pipeline:/pipeline:rw + environment: + MODE: BACKEND + DISABLE_ADDITIONAL_FEATURES: "false" + DOCKER_ENABLE_SECURITY: "false" + PUID: 1000 + PGID: 1000 + UMASK: "022" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 4G + + stirling-pdf-frontend: + container_name: Stirling-PDF-Frontend + build: + context: ../.. + dockerfile: docker/Dockerfile.unified + ports: + - "8080:8080" + environment: + MODE: FRONTEND + + # Point to the backend service + VITE_API_BASE_URL: http://stirling-pdf-backend:8080 + + # Minimal config needed for frontend + PUID: 1000 + PGID: 1000 + depends_on: + - stirling-pdf-backend + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 512M diff --git a/docker/unified/README.md b/docker/unified/README.md new file mode 100644 index 000000000..6f0488aa2 --- /dev/null +++ b/docker/unified/README.md @@ -0,0 +1,458 @@ +# Stirling-PDF Unified Container + +Single Docker container that can run as **frontend + backend**, **frontend only**, or **backend only** using the `MODE` environment variable. + +## Quick Start + +### MODE=BOTH (Default) +Single container with both frontend and backend on port 8080: + +```bash +docker run -p 8080:8080 \ + -e MODE=BOTH \ + stirlingtools/stirling-pdf:unified +``` + +Access at: `http://localhost:8080` + +### MODE=FRONTEND +Frontend only, connecting to separate backend: + +```bash +docker run -p 8080:8080 \ + -e MODE=FRONTEND \ + -e VITE_API_BASE_URL=http://backend:8080 \ + stirlingtools/stirling-pdf:unified +``` + +### MODE=BACKEND +Backend API only: + +```bash +docker run -p 8080:8080 \ + -e MODE=BACKEND \ + stirlingtools/stirling-pdf:unified +``` + +Access API at: `http://localhost:8080/api` +Swagger UI at: `http://localhost:8080/swagger-ui/index.html` + +--- + +## Architecture + +### MODE=BOTH (Default) +``` +┌─────────────────────────────────────┐ +│ Port 8080 (External) │ +│ ┌───────────────────────────────┐ │ +│ │ Nginx │ │ +│ │ • Serves frontend (/) │ │ +│ │ • Proxies /api/* → backend │ │ +│ └───────────┬───────────────────┘ │ +│ │ │ +│ ┌───────────▼───────────────────┐ │ +│ │ Backend (Internal 8081) │ │ +│ │ • Spring Boot │ │ +│ │ • PDF Processing │ │ +│ │ • UnoServer │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### MODE=FRONTEND +``` +┌─────────────────────────────┐ ┌──────────────────┐ +│ Frontend Container │ │ Backend │ +│ Port 8080 │ │ (External) │ +│ ┌───────────────────────┐ │ │ │ +│ │ Nginx │ │──────▶ :8080/api │ +│ │ • Serves frontend │ │ │ │ +│ │ • Proxies to backend │ │ │ │ +│ └───────────────────────┘ │ └──────────────────┘ +└─────────────────────────────┘ +``` + +### MODE=BACKEND +``` +┌─────────────────────────────┐ +│ Backend Container │ +│ Port 8080 │ +│ ┌───────────────────────┐ │ +│ │ Spring Boot │ │ +│ │ • API Endpoints │ │ +│ │ • PDF Processing │ │ +│ │ • UnoServer │ │ +│ └───────────────────────┘ │ +└─────────────────────────────┘ +``` + +--- + +## Environment Variables + +### MODE Configuration + +| Variable | Values | Default | Description | +|----------|--------|---------|-------------| +| `MODE` | `BOTH`, `FRONTEND`, `BACKEND` | `BOTH` | Container operation mode | + +### MODE=BOTH Specific + +| Variable | Default | Description | +|----------|---------|-------------| +| `BACKEND_INTERNAL_PORT` | `8081` | Internal port for backend when MODE=BOTH | + +### MODE=FRONTEND Specific + +| Variable | Default | Description | +|----------|---------|-------------| +| `VITE_API_BASE_URL` | `http://backend:8080` | Backend URL for API proxying | + +### Standard Configuration + +All modes support standard Stirling-PDF environment variables: + +- `DISABLE_ADDITIONAL_FEATURES` - Enable/disable OCR and LibreOffice features +- `DOCKER_ENABLE_SECURITY` - Enable authentication +- `PUID` / `PGID` - User/Group IDs +- `SYSTEM_MAXFILESIZE` - Max upload size (MB) +- `TESSERACT_LANGS` - Comma-separated OCR language codes +- `JAVA_CUSTOM_OPTS` - Additional JVM options + +See full configuration docs at: https://docs.stirlingpdf.com + +--- + +## Docker Compose Examples + +### Example 1: All-in-One (MODE=BOTH) + +**File:** `docker/compose/docker-compose-unified-both.yml` + +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:unified + ports: + - "8080:8080" + volumes: + - ./data:/usr/share/tessdata:rw + - ./config:/configs:rw + environment: + MODE: BOTH + restart: unless-stopped +``` + +### Example 2: Separate Frontend & Backend + +**File:** `docker/compose/docker-compose-unified-frontend.yml` + +```yaml +services: + backend: + image: stirlingtools/stirling-pdf:unified + ports: + - "8081:8080" + environment: + MODE: BACKEND + volumes: + - ./data:/usr/share/tessdata:rw + - ./config:/configs:rw + + frontend: + image: stirlingtools/stirling-pdf:unified + ports: + - "8080:8080" + environment: + MODE: FRONTEND + VITE_API_BASE_URL: http://backend:8080 + depends_on: + - backend +``` + +### Example 3: Backend API Only + +**File:** `docker/compose/docker-compose-unified-backend.yml` + +```yaml +services: + stirling-pdf-api: + image: stirlingtools/stirling-pdf:unified + ports: + - "8080:8080" + environment: + MODE: BACKEND + volumes: + - ./data:/usr/share/tessdata:rw + - ./config:/configs:rw + restart: unless-stopped +``` + +--- + +## Building the Image + +```bash +# From repository root +docker build -t stirlingtools/stirling-pdf:unified -f docker/Dockerfile.unified . +``` + +### Build Arguments + +| Argument | Description | +|----------|-------------| +| `VERSION_TAG` | Version tag for the image | + +Example: +```bash +docker build \ + --build-arg VERSION_TAG=v1.0.0 \ + -t stirlingtools/stirling-pdf:unified \ + -f docker/Dockerfile.unified . +``` + +--- + +## Use Cases + +### 1. Simple Deployment (MODE=BOTH) +- **Best for:** Personal use, small teams, simple deployments +- **Pros:** Single container, easy setup, minimal configuration +- **Cons:** Frontend and backend scale together + +### 2. Scaled Frontend (MODE=FRONTEND + BACKEND) +- **Best for:** High traffic, need to scale frontend independently +- **Pros:** Scale frontend containers separately, CDN-friendly +- **Example:** + ```yaml + services: + backend: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: BACKEND + deploy: + replicas: 1 + + frontend: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: FRONTEND + VITE_API_BASE_URL: http://backend:8080 + deploy: + replicas: 5 # Scale frontend independently + ``` + +### 3. API-Only (MODE=BACKEND) +- **Best for:** Headless deployments, custom frontends, API integrations +- **Pros:** Minimal resources, no nginx overhead +- **Example:** Use with external frontend or API consumers + +### 4. Multi-Backend Setup +- **Best for:** Load balancing, high availability +- **Example:** + ```yaml + services: + backend-1: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: BACKEND + + backend-2: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: BACKEND + + frontend: + image: stirlingtools/stirling-pdf:unified + environment: + MODE: FRONTEND + VITE_API_BASE_URL: http://load-balancer:8080 + ``` + +--- + +## Port Configuration + +All modes use **port 8080** by default: + +- **MODE=BOTH**: Nginx listens on 8080, proxies to backend on internal 8081 +- **MODE=FRONTEND**: Nginx listens on 8080 +- **MODE=BACKEND**: Spring Boot listens on 8080 + +**Expose port 8080** in all configurations: +```yaml +ports: + - "8080:8080" +``` + +--- + +## Health Checks + +### MODE=BOTH and MODE=BACKEND +```yaml +healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + interval: 30s + timeout: 10s + retries: 3 +``` + +### MODE=FRONTEND +```yaml +healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 +``` + +--- + +## Troubleshooting + +### Check logs +```bash +docker logs stirling-pdf-container +``` + +Look for the startup banner: +``` +=================================== +Stirling-PDF Unified Container +MODE: BOTH +=================================== +``` + +### Invalid MODE error +``` +ERROR: Invalid MODE 'XYZ'. Must be BOTH, FRONTEND, or BACKEND +``` +**Fix:** Set `MODE` to one of the three valid values. + +### Frontend can't connect to backend (MODE=FRONTEND) +**Check:** +1. `VITE_API_BASE_URL` points to correct backend URL +2. Backend container is running and accessible +3. Network connectivity between containers + +### Backend not starting (MODE=BOTH or BACKEND) +**Check:** +1. Sufficient memory allocated (4GB recommended) +2. Java heap size (`JAVA_CUSTOM_OPTS`) +3. Volume permissions for `/tmp/stirling-pdf` + +--- + +## Migration Guide + +### From Separate Containers → MODE=BOTH + +**Before:** +```yaml +services: + frontend: + image: stirlingtools/stirling-pdf:frontend + ports: ["80:80"] + + backend: + image: stirlingtools/stirling-pdf:backend + ports: ["8080:8080"] +``` + +**After:** +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:unified + ports: ["8080:8080"] + environment: + MODE: BOTH +``` + +### From Legacy → MODE=BACKEND +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:latest + ports: ["8080:8080"] +``` + +**Becomes:** +```yaml +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:unified + ports: ["8080:8080"] + environment: + MODE: BACKEND +``` + +--- + +## Performance Tuning + +### MODE=BOTH +```yaml +environment: + JAVA_CUSTOM_OPTS: "-Xmx4g -XX:MaxRAMPercentage=75" + BACKEND_INTERNAL_PORT: 8081 +deploy: + resources: + limits: + memory: 4G + reservations: + memory: 2G +``` + +### MODE=FRONTEND (Lightweight) +```yaml +deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M +``` + +### MODE=BACKEND (Heavy Processing) +```yaml +environment: + JAVA_CUSTOM_OPTS: "-Xmx8g" +deploy: + resources: + limits: + memory: 10G + reservations: + memory: 4G +``` + +--- + +## Security Considerations + +1. **MODE=BOTH**: Backend not exposed externally (runs on internal port) +2. **MODE=BACKEND**: API exposed directly - consider API authentication +3. **MODE=FRONTEND**: Only serves static files - minimal attack surface + +Enable security features: +```yaml +environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" +``` + +--- + +## Support + +- Documentation: https://docs.stirlingpdf.com +- GitHub Issues: https://github.com/Stirling-Tools/Stirling-PDF/issues +- Docker Hub: https://hub.docker.com/r/stirlingtools/stirling-pdf + +--- + +## License + +MIT License - See repository for full details diff --git a/docker/unified/build.sh b/docker/unified/build.sh new file mode 100644 index 000000000..5dd59d668 --- /dev/null +++ b/docker/unified/build.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Build script for Stirling-PDF Unified Container +# Usage: ./build.sh [version-tag] + +set -e + +VERSION_TAG=${1:-latest} +IMAGE_NAME="stirlingtools/stirling-pdf:unified-${VERSION_TAG}" + +echo "===================================" +echo "Building Stirling-PDF Unified Container" +echo "Version: $VERSION_TAG" +echo "Image: $IMAGE_NAME" +echo "===================================" + +# Navigate to repository root (assuming script is in docker/unified/) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +REPO_ROOT="$SCRIPT_DIR/../.." + +cd "$REPO_ROOT" + +# Build the image +docker build \ + --build-arg VERSION_TAG="$VERSION_TAG" \ + -t "$IMAGE_NAME" \ + -f docker/Dockerfile.unified \ + . + +echo "===================================" +echo "✓ Build complete!" +echo "Image: $IMAGE_NAME" +echo "" +echo "Test the image:" +echo " MODE=BOTH: docker run -p 8080:8080 -e MODE=BOTH $IMAGE_NAME" +echo " MODE=FRONTEND: docker run -p 8080:8080 -e MODE=FRONTEND $IMAGE_NAME" +echo " MODE=BACKEND: docker run -p 8080:8080 -e MODE=BACKEND $IMAGE_NAME" +echo "===================================" diff --git a/docker/unified/entrypoint.sh b/docker/unified/entrypoint.sh new file mode 100644 index 000000000..92075ff3a --- /dev/null +++ b/docker/unified/entrypoint.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +set -e + +# Default MODE to BOTH if not set +MODE=${MODE:-BOTH} + +echo "===================================" +echo "Stirling-PDF Unified Container" +echo "MODE: $MODE" +echo "===================================" + +# Function to setup OCR (from init.sh) +setup_ocr() { + echo "Setting up OCR languages..." + + # Copy tessdata + mkdir -p /usr/share/tessdata + cp -rn /usr/share/tessdata-original/* /usr/share/tessdata 2>/dev/null || true + + if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then + cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata 2>/dev/null || true + fi + + if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then + cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata 2>/dev/null || true + fi + + # Install additional languages if specified + if [[ -n "$TESSERACT_LANGS" ]]; then + SPACE_SEPARATED_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ') + pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$' + for LANG in $SPACE_SEPARATED_LANGS; do + if [[ $LANG =~ $pattern ]]; then + apk add --no-cache "tesseract-ocr-data-$LANG" 2>/dev/null || true + fi + done + fi +} + +# Function to setup user permissions (from init-without-ocr.sh) +setup_permissions() { + echo "Setting up user permissions..." + + export JAVA_TOOL_OPTIONS="${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}" + + # Update user and group IDs + if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then + usermod -o -u "$PUID" stirlingpdfuser || true + fi + + if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then + groupmod -o -g "$PGID" stirlingpdfgroup || true + fi + + umask "$UMASK" || true + + # Install fonts if needed + if [[ -n "$LANGS" ]]; then + /scripts/installFonts.sh $LANGS + fi + + # Ensure directories exist with correct permissions + mkdir -p /tmp/stirling-pdf || true + + # Set ownership and permissions + chown -R stirlingpdfuser:stirlingpdfgroup \ + $HOME /logs /scripts /usr/share/fonts/opentype/noto \ + /configs /customFiles /pipeline /tmp/stirling-pdf \ + /var/lib/nginx /var/log/nginx /usr/share/nginx \ + /app.jar 2>/dev/null || echo "[WARN] Some chown operations failed, may run as host user" + + chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto \ + /configs /customFiles /pipeline /tmp/stirling-pdf 2>/dev/null || true +} + +# Function to configure nginx +configure_nginx() { + local backend_url=$1 + echo "Configuring nginx with backend URL: $backend_url" + sed -i "s|\${BACKEND_URL}|${backend_url}|g" /etc/nginx/nginx.conf +} + +# Function to run as user or root depending on permissions +run_as_user() { + if [ "$(id -u)" = "0" ]; then + # Running as root, use su-exec + su-exec stirlingpdfuser "$@" + else + # Already running as non-root + exec "$@" + fi +} + +# Setup OCR and permissions +setup_ocr +setup_permissions + +# Handle different modes +case "$MODE" in + BOTH) + echo "Starting in BOTH mode: Frontend + Backend on port 8080" + + # Configure nginx to proxy to internal backend + configure_nginx "http://localhost:${BACKEND_INTERNAL_PORT:-8081}" + + # Start backend on internal port + echo "Starting backend on port ${BACKEND_INTERNAL_PORT:-8081}..." + run_as_user sh -c "java -Dfile.encoding=UTF-8 \ + -Djava.io.tmpdir=/tmp/stirling-pdf \ + -Dserver.port=${BACKEND_INTERNAL_PORT:-8081} \ + -jar /app.jar" & + BACKEND_PID=$! + + # Start unoserver for document conversion + run_as_user /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1 & + UNO_PID=$! + + # Wait for backend to start + sleep 3 + + # Start nginx on port 8080 + echo "Starting nginx on port 8080..." + run_as_user nginx -g "daemon off;" & + NGINX_PID=$! + + echo "===================================" + echo "✓ Frontend available at: http://localhost:8080" + echo "✓ Backend API at: http://localhost:8080/api" + echo "✓ Backend running internally on port ${BACKEND_INTERNAL_PORT:-8081}" + echo "===================================" + ;; + + FRONTEND) + echo "Starting in FRONTEND mode: Frontend only on port 8080" + + # Configure nginx with external backend URL + BACKEND_URL=${VITE_API_BASE_URL:-http://backend:8080} + configure_nginx "$BACKEND_URL" + + # Start nginx on port 8080 + echo "Starting nginx on port 8080..." + run_as_user nginx -g "daemon off;" & + NGINX_PID=$! + + echo "===================================" + echo "✓ Frontend available at: http://localhost:8080" + echo "✓ Proxying API calls to: $BACKEND_URL" + echo "===================================" + ;; + + BACKEND) + echo "Starting in BACKEND mode: Backend only on port 8080" + + # Start backend on port 8080 + echo "Starting backend on port 8080..." + run_as_user sh -c "java -Dfile.encoding=UTF-8 \ + -Djava.io.tmpdir=/tmp/stirling-pdf \ + -Dserver.port=8080 \ + -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1" & + BACKEND_PID=$! + + echo "===================================" + echo "✓ Backend API available at: http://localhost:8080/api" + echo "✓ Swagger UI at: http://localhost:8080/swagger-ui/index.html" + echo "===================================" + ;; + + *) + echo "ERROR: Invalid MODE '$MODE'. Must be BOTH, FRONTEND, or BACKEND" + exit 1 + ;; +esac + +# Wait for all background processes +wait diff --git a/docker/unified/nginx.conf b/docker/unified/nginx.conf new file mode 100644 index 000000000..77ee17f89 --- /dev/null +++ b/docker/unified/nginx.conf @@ -0,0 +1,118 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Add .mjs MIME type mapping + types { + text/javascript mjs; + } + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html index.htm; + + # Global settings for file uploads + client_max_body_size 100m; + + # Handle client-side routing - support subpaths + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API calls to backend + location /api/ { + proxy_pass ${BACKEND_URL}/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Additional headers for proper API proxying + proxy_set_header Connection ''; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + + # Timeout settings for large file uploads + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Request size limits for file uploads + client_max_body_size 100m; + proxy_request_buffering off; + } + + # Proxy Swagger UI to backend (including versioned paths) + location ~ ^/swagger-ui(.*)$ { + proxy_pass ${BACKEND_URL}/swagger-ui$1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_set_header Connection ''; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + } + + # Proxy API docs to backend (with query parameters and sub-paths) + location ~ ^/v3/api-docs(.*)$ { + proxy_pass ${BACKEND_URL}/v3/api-docs$1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + # Proxy v1 API docs to backend (with query parameters and sub-paths) + location ~ ^/v1/api-docs(.*)$ { + proxy_pass ${BACKEND_URL}/v1/api-docs$1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + # Serve .mjs files with correct MIME type (must come before general static assets) + location ~* \.mjs$ { + try_files $uri =404; + add_header Content-Type "text/javascript; charset=utf-8" always; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + } +} From 960d48f80c191513cbf34bb3deea24b5d7c7557a Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:54:59 +0000 Subject: [PATCH 9/9] Customised Analytics for admins and users (#4687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds granular privacy controls for analytics - splits single enableAnalytics toggle into separate PostHog and Scarf controls with improved admin UX. Backend Changes Configuration (ApplicationProperties.java) - Added enablePosthog and enableScarf boolean fields - New methods: isPosthogEnabled(), isScarfEnabled() (null = enabled when analytics is on) Services - PostHogService: Now checks isPosthogEnabled() instead of isAnalyticsEnabled() - ConfigController: Exposes new flags via API - SettingsController: Changed endpoint from @RequestBody to @RequestParam Frontend Changes Architecture - Converted useAppConfig hook → AppConfigContext provider for global access - Added refetch() method for config updates without reload New Features 1. AdminAnalyticsChoiceModal: First-launch modal when enableAnalytics === null - Enable/disable without editing YAML - Includes documentation link 2. Scarf Tracking System: Modular utility with React hook wrapper - Respects config + per-service cookie consent - Works from any code location (React or vanilla JS) 3. Enhanced Cookie Consent: Per-service toggles (PostHog and Scarf separate) Integration - App.tsx: Added AppConfigProvider + scarf initializer - HomePage.tsx: Shows admin modal when needed - index.tsx: PostHog opt-out by default, service-level consent Key Benefits ✅ Backward compatible (null defaults to enabled) ✅ Granular control per analytics service ✅ First-launch admin modal (no YAML editing) ✅ Privacy-focused with opt-out defaults ✅ API-based config updates --------- Co-authored-by: Connor Yoh --- .../common/model/ApplicationProperties.java | 14 +++ .../common/service/PostHogService.java | 12 +- .../controller/api/SettingsController.java | 4 +- .../controller/api/misc/ConfigController.java | 2 + .../src/main/resources/settings.yml.template | 4 +- frontend/package-lock.json | 42 +------ .../public/locales/en-GB/translation.json | 5 + frontend/src/App.tsx | 59 +++++---- frontend/src/components/layout/Workbench.tsx | 4 +- .../shared/AdminAnalyticsChoiceModal.tsx | 112 ++++++++++++++++++ .../src/components/shared/QuickAccessBar.tsx | 2 +- .../shared/config/configSections/Overview.tsx | 4 +- .../certSign/CertificateTypeSettings.tsx | 2 +- .../AppConfigContext.tsx} | 42 +++++-- frontend/src/hooks/useBaseUrl.ts | 2 +- frontend/src/hooks/useCookieConsent.ts | 49 ++++++-- frontend/src/hooks/useScarfTracking.ts | 44 +++++++ frontend/src/index.tsx | 23 ++-- frontend/src/pages/HomePage.tsx | 15 +++ frontend/src/routes/Landing.tsx | 10 +- frontend/src/styles/zIndex.ts | 3 +- frontend/src/utils/scarfTracking.ts | 73 +++++++++++- 22 files changed, 418 insertions(+), 109 deletions(-) create mode 100644 frontend/src/components/shared/AdminAnalyticsChoiceModal.tsx rename frontend/src/{hooks/useAppConfig.ts => contexts/AppConfigContext.tsx} (62%) create mode 100644 frontend/src/hooks/useScarfTracking.ts diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index e9230185b..a8861fdd8 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -355,6 +355,8 @@ public class ApplicationProperties { private String tessdataDir; private Boolean enableAlphaFunctionality; private Boolean enableAnalytics; + private Boolean enablePosthog; + private Boolean enableScarf; private Datasource datasource; private Boolean disableSanitize; private int maxDPI; @@ -368,6 +370,18 @@ public class ApplicationProperties { public boolean isAnalyticsEnabled() { return this.getEnableAnalytics() != null && this.getEnableAnalytics(); } + + public boolean isPosthogEnabled() { + // Treat null as enabled when analytics is enabled + return this.isAnalyticsEnabled() + && (this.getEnablePosthog() == null || this.getEnablePosthog()); + } + + public boolean isScarfEnabled() { + // Treat null as enabled when analytics is enabled + return this.isAnalyticsEnabled() + && (this.getEnableScarf() == null || this.getEnableScarf()); + } } @Data diff --git a/app/common/src/main/java/stirling/software/common/service/PostHogService.java b/app/common/src/main/java/stirling/software/common/service/PostHogService.java index 2bc219832..0d6353b50 100644 --- a/app/common/src/main/java/stirling/software/common/service/PostHogService.java +++ b/app/common/src/main/java/stirling/software/common/service/PostHogService.java @@ -56,7 +56,7 @@ public class PostHogService { } private void captureSystemInfo() { - if (!applicationProperties.getSystem().isAnalyticsEnabled()) { + if (!applicationProperties.getSystem().isPosthogEnabled()) { return; } try { @@ -67,7 +67,7 @@ public class PostHogService { } public void captureEvent(String eventName, Map properties) { - if (!applicationProperties.getSystem().isAnalyticsEnabled()) { + if (!applicationProperties.getSystem().isPosthogEnabled()) { return; } @@ -325,6 +325,14 @@ public class PostHogService { properties, "system_enableAnalytics", applicationProperties.getSystem().isAnalyticsEnabled()); + addIfNotEmpty( + properties, + "system_enablePosthog", + applicationProperties.getSystem().isPosthogEnabled()); + addIfNotEmpty( + properties, + "system_enableScarf", + applicationProperties.getSystem().isScarfEnabled()); // Capture UI properties addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName()); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java index 3c2e6f33a..48793a98b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -6,7 +6,7 @@ import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import io.swagger.v3.oas.annotations.Hidden; @@ -29,7 +29,7 @@ public class SettingsController { @AutoJobPostMapping("/update-enable-analytics") @Hidden - public ResponseEntity updateApiKey(@RequestBody Boolean enabled) throws IOException { + public ResponseEntity updateApiKey(@RequestParam Boolean enabled) throws IOException { if (applicationProperties.getSystem().getEnableAnalytics() != null) { return ResponseEntity.status(HttpStatus.ALREADY_REPORTED) .body( 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 072471e5c..9d94c3ad4 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 @@ -65,6 +65,8 @@ public class ConfigController { applicationProperties.getSystem().getEnableAlphaFunctionality()); configData.put( "enableAnalytics", applicationProperties.getSystem().getEnableAnalytics()); + configData.put("enablePosthog", applicationProperties.getSystem().getEnablePosthog()); + configData.put("enableScarf", applicationProperties.getSystem().getEnableScarf()); // Premium/Enterprise settings configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled()); diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 1e577a368..a9055833a 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -120,7 +120,9 @@ system: showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true' customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored. - enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true + enableAnalytics: null # Master toggle for analytics: set to 'true' to enable all analytics, 'false' to disable all analytics, or leave as 'null' to prompt admin on first launch + enablePosthog: null # Enable PostHog analytics (open-source product analytics): set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled + enableScarf: null # Enable Scarf tracking pixel: set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML) maxDPI: 500 # Maximum allowed DPI for PDF to image conversion diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7dd9e15cd..6a769c90b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -436,7 +436,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -483,7 +482,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -507,7 +505,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz", "integrity": "sha512-lE/vfhA53CxamaCfGWEibrEPr+JeZT42QCF+cOELUwv4+Zt6b+IE6+4wsznx/8wjjJYwllXJ3GJ/un1UzTqARw==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/engines": "1.3.14", "@embedpdf/models": "1.3.14" @@ -588,7 +585,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.14.tgz", "integrity": "sha512-77hnNLp0W0FHw8lT7SeqzCgp8bOClfeOAPZdcInu/jPDhVASUGYbtE/0fkLhiaqPH7kyMirNCLif4sF6n4b5vg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -605,7 +601,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.14.tgz", "integrity": "sha512-nR0ZxNoTQtGqOHhweFh6QJ+nUJ4S4Ag1wWur6vAUAi8U95HUOfZhOEa0polZo0zR9WmmblGqRWjFM+mVSOoi1w==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -622,7 +617,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.14.tgz", "integrity": "sha512-KoJX1MacEWE2DrO1OeZeG/Ehz76//u+ida/xb4r9BfwqAp5TfYlksq09cOvcF8LMW5FY4pbAL+AHKI1Hjz+HNA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -657,7 +651,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.14.tgz", "integrity": "sha512-IPj7GCQXJBsY++JaU+z7y+FwX5NaDBj4YYV6hsHNtSGf42Y1AdlwJzDYetivG2bA84xmk7KgD1X2Y3eIFBhjwA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -690,7 +683,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.14.tgz", "integrity": "sha512-fQbt7OlRMLQJMuZj/Bzh0qpRxMw1ld5Qe/OTw8N54b/plljnFA52joE7cITl3H03huWWyHS3NKOScbw7f34dog==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -725,7 +717,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.14.tgz", "integrity": "sha512-EXENuaAsse3rT6cjA1nYzyrNvoy62ojJl28wblCng6zcs3HSlGPemIQZAvaYKPUxoY608M+6nKlcMQ5neRnk/A==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -797,7 +788,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.14.tgz", "integrity": "sha512-mfJ7EbbU68eKk6oFvQ4ozGJNpxUxWbjQ5Gm3uuB+Gj5/tWgBocBOX36k/9LgivEEeX7g2S0tOgyErljApmH8Vg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.3.14" }, @@ -951,7 +941,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -995,7 +984,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2029,7 +2017,6 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.1.tgz", "integrity": "sha512-OYfxn9cTv+K6RZ8+Ozn/HDQXkB8Fmn+KJJt5lxyFDP9F09EHnC59Ldadv1LyUZVBGtNqz4sn6b3vBShbxwAmYw==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", @@ -2080,7 +2067,6 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.1.tgz", "integrity": "sha512-lQutBS+Q0iz/cNFvdrsYassPWo3RtWcmDGJeOtKfHigLzFOhxUuLOkQgepDbMf3WcVMB/tist6Px1PQOv57JTw==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2148,7 +2134,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.2.tgz", "integrity": "sha512-qXvbnawQhqUVfH1LMgMaiytP+ZpGoYhnGl7yYq2x57GYzcFL/iPzSZ3L30tlbwEjSVKNYcbiKO8tANR1tadjUg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.3", "@mui/core-downloads-tracker": "^7.3.2", @@ -3591,7 +3576,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3915,7 +3899,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3926,7 +3909,6 @@ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -3987,7 +3969,6 @@ "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", @@ -4701,6 +4682,7 @@ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz", "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/shared": "3.5.21" } @@ -4710,6 +4692,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz", "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.21", "@vue/shared": "3.5.21" @@ -4720,6 +4703,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.21", "@vue/runtime-core": "3.5.21", @@ -4732,6 +4716,7 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz", "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.21", "@vue/shared": "3.5.21" @@ -4759,7 +4744,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5434,7 +5418,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -6438,8 +6421,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -6834,7 +6816,6 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7006,7 +6987,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8280,7 +8260,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -9077,7 +9056,6 @@ "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^6.5.4", "cssstyle": "^5.3.0", @@ -10866,7 +10844,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11158,7 +11135,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -11531,7 +11507,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11541,7 +11516,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13179,7 +13153,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13462,7 +13435,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13545,7 +13517,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -13765,7 +13736,6 @@ "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -13897,7 +13867,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13911,7 +13880,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 904817c75..973717879 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -250,6 +250,7 @@ "title": "Do you want make Stirling PDF better?", "paragraph1": "Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.", "paragraph2": "Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.", + "learnMore": "Learn more", "enable": "Enable analytics", "disable": "Disable analytics", "settings": "You can change the settings for analytics in the config/settings.yml file" @@ -3371,6 +3372,10 @@ "title": "Analytics", "description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with." } + }, + "services": { + "posthog": "PostHog Analytics", + "scarf": "Scarf Pixel" } }, "removeMetadata": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fd4d466ad..ddadbc8c8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,10 +9,12 @@ import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext"; import { HotkeyProvider } from "./contexts/HotkeyContext"; import { SidebarProvider } from "./contexts/SidebarContext"; import { PreferencesProvider } from "./contexts/PreferencesContext"; +import { AppConfigProvider } from "./contexts/AppConfigContext"; import { OnboardingProvider } from "./contexts/OnboardingContext"; import { TourOrchestrationProvider } from "./contexts/TourOrchestrationContext"; import ErrorBoundary from "./components/shared/ErrorBoundary"; import OnboardingTour from "./components/onboarding/OnboardingTour"; +import { useScarfTracking } from "./hooks/useScarfTracking"; // Import auth components import { AuthProvider } from "./auth/UseSession"; @@ -48,6 +50,12 @@ const LoadingFallback = () => ( ); +// Component to initialize scarf tracking (must be inside AppConfigProvider) +function ScarfTrackingInitializer() { + useScarfTracking(); + return null; +} + export default function App() { return ( }> @@ -66,30 +74,33 @@ export default function App() { path="/*" element={ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + } /> diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 4ec7d8017..da4447a70 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -6,6 +6,7 @@ import { useFileState } from '../../contexts/FileContext'; import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext'; import { isBaseWorkbench } from '../../types/workbench'; import { useViewer } from '../../contexts/ViewerContext'; +import { useAppConfig } from '../../contexts/AppConfigContext'; import './Workbench.css'; import TopControls from '../shared/TopControls'; @@ -20,6 +21,7 @@ import DismissAllErrorsButton from '../shared/DismissAllErrorsButton'; // No props needed - component uses contexts directly export default function Workbench() { const { isRainbowMode } = useRainbowThemeContext(); + const { config } = useAppConfig(); // Use context-based hooks to eliminate all prop drilling const { selectors } = useFileState(); @@ -188,7 +190,7 @@ export default function Workbench() { {renderMainContent()} -